vue computed 源码解析
引入:和 ref 的对比
如果你已经对ref
的源码有了一定的了解,那么computed
的源码看起来是很简单的,其逻辑和ref
的逻辑大致相同,只在一些地方存在区别:
-
ref
在getter
中收集依赖,在setter
中触发依赖;而computed
一般是没有setter
的,如果用户在定义计算属性时给定了setter
,computed
在setter
中也只是简单的执行用户传入的setter
,而不会显式地触发依赖export class ComputedRefImpl<T> { /* 省略其他代码 */ set value(newValue: T) { this._setter(newValue) } }
-
computed
对象既是一种ref
,也是副作用。和ref
相同,通过value
属性可以访问计算属性的值;和副作用相同,计算属性有effect
属性,当计算属性的依赖发生改变时,它会被触发:// 初始化计算属性的 effect 属性,参数分别为 fn 和 调度器 scheduler this.effect = new ReactiveEffect(getter, () => { if (!this._dirty) { this._dirty = true triggerRefValue(this) } })
但是
computed
区别于一般副作用的一点是,当一般副作用的依赖发生改变时,这个副作用会被重新调用;而当computed
的依赖改变时,它的getter
并不会被调用,而是触发依赖该计算属性的副作用,让它们重新执行,只有当副作用访问到该计算属性时,才会更新计算属性的值
正文
计算属性的创建
计算属性的创建流程的第一步是调用computed
函数,该函数会处理用户的传入,并使用这些参数实例化ComputedRefImpl
类对象
逻辑很容易看懂,就是拿到用户传入的getter
和setter
,如果没有setter
则指定警告或空函数,将这些传入构造函数得到cRef
并返回
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack
cRef.effect.onTrigger = debugOptions.onTrigger
}
return cRef as any
}
计算属性创建的第二步是在构造函数中设置this.effect
,其中第一个参数getter
会在effect.run
中被调用,第二个参数是副作用的调度器,当响应式变量更改,从而更新计算属性时,调度器会在triggerEffect
函数中被执行
export class ComputedRefImpl<T> {
/* 省略其他代码 */
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
}
计算属性的更新过程
那么,当计算属性的依赖发生改变时,计算属性的值是如何更新的,又是怎样触发依赖它的副作用的呢?
事实上,依赖收集的过程和ref
是相同的,都是在getter
中通过trackRefValue
收集依赖。
而依赖触发的过程则完全不同,当计算属性的依赖发生改变时,会触发它的调度器,这首先会设置计算属性的_dirty
属性,表示该计算属性是“脏”的,即它的值需要更新;接着会调用triggerRefValue
触发计算属性的依赖,在它的依赖被调用的时候,会访问到计算属性的值,从而会触发getter
:
export class ComputedRefImpl<T> {
/* 省略其他代码 */
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self)
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
在getter
中,如果发现值是需要更新的,则会调用effect.run
,从而重新调用用户传入的getter
,更新自身的值
setter 相关逻辑
在computed
的setter
中,我们一般是更新该计算属性的依赖,例如
const val = ref(1)
const cVal = computed({
get() {
return val.value * 2
}
set(newValue) {
val.value = newValue / 2
}
})
在这个setter
中,我们更新了val
,由于cVal
是该变量的依赖,所以会被触发,从而会更新它的值。这也就是为什么源码在setter
中只是调用用户传入的setter
而没有触发依赖