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,从而会导致计算属性的调度器被执行,因此使用到计算属性的副作用会被重新执行,在重新执行过程中通过计算属性的getter更新cVal.value。由于cVal是val的依赖,所以当val改变时它会被自动触发,从而会更新它的值。这也就是为什么源码在setter中只是调用用户传入的setter而没有触发依赖。