前言#
昨天我们分析完了ref和reactive
坏蛋Dan:vue runtime源码分析学习——响应式原理day1: ref和reactive
今天我们来分析computed做了什么。
computed#
代码位置:packages\reactivity\src\computed.ts
export function computed<T>(
getter: ComputedGetter<T>,
debugOptions?: DebuggerOptions
): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
): WritableComputedRef<T>
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
}上来三个函数重载,分别对应computed的两种场景:
- 非
SSR传入回调的方式/传入对象包含getter/setter。 SSR传入回调的方式/对象包含getter/setter。
SSR的我们照旧不做分析。另外debugger相关的我这里也不分析了,感兴趣的大佬可自行看下源码。
如果第一个参数是函数的话,那就是传入回调的方式,这个时候没有setter,只有getter,getter就是回调自身,所以传回调的方式一定要有return值才行。
重点是则个ComputedRefImpl类做了什么。
我们来看下
ComputedRefImpl#
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly __v_isReadonly: boolean = false
public _dirty = true
public _cacheable: boolean
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.__v_isReadonly = isReadonly
}
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)
}
}代码挺短的,但是里面的原理还是比较复杂的。
_dirty:这个字段后面我们需要特别关注。
在初始化也就是构造函数里创建了一个effect,ReactiveEffect这块的内容我们之前在分析processComponent的时候就分析过了,这里简单地说就是它里面有个run方法可以用来触发我们传入的第一个参数也就是更新的时候需要执行的回调,并且这个过程中伴随着旧依赖的移除和新依赖的加入。
getter就是我们传入进来的回调,执行会返回我们需要监听的数据。而这个getter到时会被effect.run触发,然后返回我们监听的数据。
ReactiveEffect的第二个参数是调度器(scheduler),在triggerEffect也就是触发effect的时候优先执行调度器,没有才执行effect.run,也就是第一个回调参数,不过调度器实际上做的事也是在执行effect.run,不过多了一些其它逻辑。比如组件的渲染多了一层queueJob(update),需要将effect放到任务调度队列中按顺序等待清洗。
- 注意这里判断如果
_ditry不是true就将它置为true,我们等会会再次遇到这个字段。 triggerRefValue:这个方法我们昨天分析ref的时候遇到了,这里就不再多说了,它里面就是会去触发上面说到的triggerEffect方法然后触发调度器或者effect.run。
然后我们来到get value方法
getter自然是收集effect,所以这里有trackRefValue,这个方法前面讲ref的时候也说过了,这里也不多说。
不过需要注意的一点是这里的收集是收集对computed自身有依赖的effect,而不是监听的对象的effect。
比如你用computed包裹了a这个ref然后赋值给b:const b = computed(() => a.value),那么这个时候如果别的地方调用b.value,那么收集的应该是b的,而不是a的,实际上也没必要去给收集a这些依赖b的,当a被set导致需要更新的时候就一定会去触发b,从而引起依赖于b的effect更新。
至于set value,它做的事情就是普通的setter。
_setter就是我们前面传入的setter参数,当没有传的时候遇到赋值会直接warning(dev mode)。
可能你会疑惑为什么这里不需要trigger了,实际上还是有trigger的,但不是computed来触发这个trigger,而是被监听的数据自己被set的时候触发。
那么到这你应该就已经发现华点了,我们的computed就和它的用法一样,实际上就是一层代理。我的依赖归我自己,不会影响被包裹的对象,但是包裹对象变了,那它也得跟着变。
然后注意这里的self._dirty(_cacheable这个和ssr相关的,这里就不多说了),如果get也就是读这个数据时_dirty是true,那么这个时候将它置为false,然后重新触发effect.run,也就是执行前面传入的getter也是传给ReactiveEffect的第一个参数。
换句话说,就是只有_dirty是true的时候才会去执行这个self.effect.run方法重新获取数据。
其它时候你访问这个数据的时候都是直接return self._value。
那么什么时候会是true呢?
- 当
self.effect.scheduler被触发的时候,也就是我们前面提到的ReactiveEffect的第二个参数。
那么这个scheduler会在什么时候触发呢?
- 我们前面说过
triggerEffect中会触发这个scheduler,而triggerEffect由trigger方法触发。
这个trigger是computed自身的?
- 不是,它来自被包裹的对象。
但是它是如何建立和被包裹的对象的关系的呢?
- 我们的
get value触发的trackRefValue方法然后触发的trackEffect,它里面会去给activeEffect和当前effect双向奔赴。
那么一切就都通了。
回到上面的例子const b = computed(() => a.value)中,当我们第一次触发get value都时候也就是读b.value的时候,这个时候因为_dirty标志位为true,所以初始会去读一次数据,但是下次再读的时候因为_dirty是false,并不会去触发effect.run方法读取包裹对象也就是a.value。
这样就实现了缓存逻辑,不管后面是哪个访问的b.value,它都只会去读缓存。
并且这个时候b的effect和a的effect建立了联系。
然后当a也就是被包裹的对象数据发生了改变,这个时候它收集的effect就会被触发,里面也包括了我们的b,那么b的effect也会被触发,优先触发的是scheduler方法,这个方法中将_dirty置为true表示这个computed已经肮脏了需要更新。那么下一次get value的时候,会重新触发effect.run方法更新_value,然后又是继续缓存直到下一次a发生变化。
举个栗子#
- 你是老板的助理,你按老板的意思把公司的招聘广告发了出去。那些前来应聘的人一般也就只能和你沟通了,在你们沟通的时候你们互相留了对方的信息。
相当于:effect相互建立联系
- 而你不用告知你的老板有这么一个人
相当于:对computed自身的依赖不需要同步给computed包裹的对象。
- 对于老板的指令,你只需要第一次去主动确认,后面除非老板自己去通知你,不然你不需要去再次确认。
相当于:初始_dirty为true,触发传入的回调获取值存储在_value里,然后_dirty置为false。此后如果_dirty都是false,那么不再需要触发传入的回调而是直接返回_value
- 老板看到了新闻突然换了想法,通知了你和他另外的助理或者亲信
- 你和其它助理:都是包裹这个对象的
computed。 - 老板的亲信:直接依赖于对象自身的数据。
- 老板看到了新闻:对象被
set了。 - 突然换了想法:对象被
set了导致数据发生了变化。 - 老板通知了助理和亲信:触发了
effect更新。
- 你得到最新的消息之后主动去告知之前和你互换信息的人
相当于:执行了你自己的effect.run去触发依赖于自身的数据更新
- 但是这个时候我们的海报还没更新,直到有人再来看这个招聘广告发现过时了,然后告诉你,这个时候你就得去修改这个招聘广告了。(不是很恰当)
相当于:再次get value读数据的时候,此时_ditry被置为true,那么这个时候就得重新触发effect.run去读取最新的数据。
总结#
这块以前看别人的分析老是觉得很难懂,现在自己来分析之后发现其实还算简单的。
这里有一个理解点需要注意:computed是computed自己,被包裹的对象是被包裹的对象自己。computed自身也会收集依赖于它的effect,但是他自己没有set的触发triggerEffect的逻辑,能通过set触发trigger的只有被包裹的对象。
我的这个例子应该挺好懂的吧。。。我觉得挺恰当的。
参考#
编辑于 2023-03-22 19:52・IP 属地广东
