computed 你可能不知道的一些特性
computed大家应该都用过,大概是下面这样子的。
const { reactive, computed } = Vue
const person = reactive({
firstname: "zhang",
lastname: "xx"
})
const personInfo = computed(() => {
return person.firstname + person.lastname
})
好,那么问题来了。
1. computed只能接受函数吗?
2. computed返回的值是什么类型,可以修改吗?
3. computed是怎么实现,数据变化,重新计算的?
如果你对这些答案不是很确定,那么这篇文章都会从源码的角度告诉你答案。
effect侦听器回顾
在我们自己动手开始实现computed功能之前,我们先回顾一下,上一讲中的effect侦听器。
在上一篇数据响应式中,我们创建了一个effect侦听器,并实现了数据的监听功能,当触发person.name的set事件时,会重新触发console.log打印。
这部分如果有不清楚的,建议先看这里
文章代码可以看这里选择对应的分支即可
function effect(fn,options = {}) {
const effectFn = () => {
try {
activeEffect = effectFn
return fn()
} catch (error) {
activeEffect = null
}
}
if (!options.lazy) {
effectFn()
}
effectFn.scheduler = options.scheduler
return effectFn
}
effect(() => {
console.log('effect person', person.name)
})
setTimeout(() => {
person.name = 'setTimeout world'
}, 2000)
在函数的定义中,我们可以看出来,effect函数除了fn还接收一个options参数。
该参数中有两个值,一个lazy,一个scheduler。
lazy很好理解,就是第一次是否执行effectFn。跟watch中的immediate是一个意思。
scheduler可以理解为调度器,会优先执行,在触发依赖的时候会判断,优先执行scheduler
effect(() => {
console.log('effect person', person.name)
}, {
scheduler: () => {
console.log('scheduler')
}
})
setTimeout(() => {
person.name = 'setTimeout world'
}, 2000)
// 2s 后输出scheduler
为了跟源码保持一致,我们对effect做一点改造。
export class ReactiveEffect<T = any> {
deps: Dep[] = []
computed?: ComputedRefImpl<T>
// 如果当前是自己,则延迟清理
private deferStop?: boolean
// 监听停止
onStop?: () => void
// 是否停止
active = true
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null
) {}
run() {
if (!this.active) {
return this.fn()
}
try {
activeEffect = this
return this.fn()
} finally {
if (this.deferStop) {
this.stop()
}
}
}
stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
主要就是run方法,内部的实现跟effect是一样的。
核心就是两行代码
activeEffect = this
return this.fn()
回顾到这,你肯定知道了,可以用effect来监听呀。
是不是呢?我们来看看源码是如何实现的。
手写一个computed
首先,从使用方法上,我们很容易可以分析出来,computed是一个函数,并返回一个值。
那这个option是什么格式呢?
我们可以看一下官网的说明。
参数可以接收函数,跟对象,返回值是一个只读的ref的对象。
有了这两点,就可以去写我们的函数了。
function computed(options) {
let getter
let setter
if(typeof options === 'function') {
getter = options
setter = () => console.warn('哥们是只读的')
} else {
getter = options.getter
setter = options.setter
}
// 返回的ref,跟之前一样,也用类创建
const cRef = new ComputedRefImpl(getter, setter)
return cRef
}
ReactiveEffect其实就是之前的effet函数。
export class ComputedRefImpl<T> {
public readonly effect: ReactiveEffect<T>
public _dirty = true
private _value!: T
constructor(getter, private readonly _setter, isReadonly: boolean) {
this.effect = new ReactiveEffect(getter, () => {
// 判断当前脏的状态,如果为 false,表示需要《触发依赖》
if (!this._dirty) {
// 将脏置为 true,表示
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
}
get value() {
// 收集依赖
trackRefValue(this)
if (this._dirty) {
this._dirty = false
this._value = this.effect.run()
}
return this._value
}
set value(newValue) {
this._setter(newValue)
}
}
这个代码也不复杂。
首先是加一个_dirty的变量来控制,当第一次触发get的时候,执行一遍run,也就是computed里面的计算函数。
当第二次访问的时候,因为_dirty已经改为false了,所以不会重新计算。
当computed里面的值发生变化的时候,则走到scheduler里面的逻辑,触发一次triggerRefValue,重新执行一遍计算。
到这里,看起来没有什么问题了,我们写个demo试一下。
const { reactive, effect, ref, computed } = Vue
const obj = reactive({
name: '张三'
})
// C1
const computedObj = computed(() => {
console.log('computed')
return '姓名:' + obj.name
})
// e1
effect(() => {
document.querySelector('#app').innerHTML = computedObj.value
})
setTimeout(() => {
obj.name = '李四'
}, 2000)
我们来看看结果,执行了两次计算,是符合预期的。
我们再试试访问两次,看有没有什么不同。
因为从代码中我们可以看到,由于_dirty的控制,computed多次访问是不会触发重新执行的,只有改变值才会重新执行。
我们把e1改成如下
effect(() => {
document.querySelector('#app').innerHTML = computedObj.value
document.querySelector('#app').innerHTML = computedObj.value
})
预期应该是计算三次对吧,但是实际上会一直执行计算。
这个问题解释起来比较复杂,下一篇我们再聊。
总结
computed的实现,主要是通过effect来监听数值变化,通过_dirty变量来控制是否需要重新计算。