effect 和 computed 是响应式系统两大核心功能,之前文章分析了双向链表架构下的 effect 是如何运作的,这篇文章主要是分析 computed 的流程,computed 是比较特殊的,既是 dep,又是 sub。比如这个 computed1,对于 obj.a 来说,它是 sub,而对于 computed2 来说,它是 dep
const computed1 = computed(() => {
console.log("computed 1")
return obj.a * 0
})
const computed2 = computed(() => {
console.log("computed 2")
return computed1.value + obj.c
})
依赖追踪整体结构
调试代码
import { reactive, effect, computed } from '@vue/reactivity'
const obj = reactive({
a: 1,
b: 2,
c: 3,
d: 4
})
const computed1 = computed(() => {
console.log("computed 1")
return obj.a * 0
})
const computed2 = computed(() => {
console.log("computed 2")
return computed1.value + obj.c
})
effect(() => {
console.log("effect 1")
computed2.value
})
debugger
obj.a++
依赖追踪结构图
具体分析
obj.a++触发更新,根据依赖关系找到对应的computed1 effect,这个computed1又是一个 dep,订阅者是computed2 effect,所以暂时认为这个computed1 dep也更新了,那就要通知computed2 effect,同样,computed2 dep通知effect1,由于batch()逻辑,通知这些 effect 的后,并不会马上执行,而是把这些 effect 函数串成一个链,链表方向和通知方向刚好是相反的
graph LR
Node1[effect1] -->|nex| Node2[computed2 effect]
Node2[computed2 effect] -->|nex| Node3[computed1 effect]
-
这种 effect 执行顺序,有效避免了 computed 的无效计算,毕竟 computed 可能缓存了一些昂贵的计算结果。
-
effect 执行的时候,调用的是
runIfDirty函数,确认isDirty才会真正执行function isDirty(sub) { for (let link = sub.deps; link; link = link.nextDep) { if (link.dep.version !== link.version || link.dep.computed && (refreshComputed(link.dep.computed) || link.dep.version !== link.version)) { return true; } } if (sub._dirty) { return true; } return false; } -
isDirty里面,如果 dep(这里的 dep 就是 computed2) 是一个 computed 类型,会执行refreshComputed -
略过一下各种
version的判断,会再次进入isDirty函数,注意步骤 a 进入isDirty函数的时候,computed2的身份是 dep,这次进入isDirty函数,computed2的身份是 sub,因为effect1问computed2有没有更新,computed2自己是不知道的,computed2会更新的前提应该是它的 effect 被重新调用,只有computed effect2重新调用,computed2的值才有可能发生变化graph LR Node1[effect1执行] -->|取决于| Node2[computed2 dep] Node2[computed2 dep更新]-->|取决于| Node3[computed2 effect 重新执行] -
所以到这里有点类似递归了,
computed effect2会不会被执行,又需要问computed1 dep,computed1为了回答问题,需要去问obj.a这个 dep,到这里终于停止了graph LR Node1[computed2 effect重新执行] -->|取决于| Node2[computed1 dep] Node2[computed1 dep更新]-->|取决于| Node3[computed1 effect 重新执行] Node3[computed1 effect 重新执行]-->|取决于| Node4[obj.a 更新] -
obj.a肯定是更新了,所以computed1 effect一定会重新执行,但是 computed1 effect 重新执行后,computed1 的 value 不会变,任何数*0 还是 0 -
这就导致了
computed1 dep没有更新,那么computed2 effect不会重新执行,computed2 dep肯定也没有更新,最后就是effect1准备重新执行前,isDirty是 false,直接中断 -
以上的流程就像是一个递归函数,层层询问 dep 有没有更新,直到最内层的 dep 才能得出是否有更新的结论,而在归的过程中,一旦发现某一层的 dep 并没有更新,相当于直接终止了更新。
-
附上
refreshComputed源码function refreshComputed(computed) { if (computed.flags & 4 && !(computed.flags & 16)) { return; } computed.flags &= ~16; if (computed.globalVersion === globalVersion) { return; } computed.globalVersion = globalVersion; const dep = computed.dep; computed.flags |= 2; if (dep.version > 0 && !computed.isSSR && computed.deps && !isDirty(computed)) { computed.flags &= ~2; return; } const prevSub = activeSub; const prevShouldTrack = shouldTrack; activeSub = computed; shouldTrack = true; try { prepareDeps(computed); const value = computed.fn(computed._value); if (dep.version === 0 || shared.hasChanged(value, computed._value)) { computed._value = value; dep.version++; } } catch (err) { dep.version++; throw err; } finally { activeSub = prevSub; shouldTrack = prevShouldTrack; cleanupDeps(computed, true); computed.flags &= ~2; } }
其他
-
effect 调用是这样的一个链表,根据上面的分析,在 effect1 调用前的 isDirty 逻辑里面,computed1 effect 其实是先调用,但是有 flag标记,避免 effect1 调用完后重复调用 computed2 effect 和 computed1 effect
graph LR Node1[effect1] -->|nex| Node2[computed2 effect] Node2[computed2 effect] -->|nex| Node3[computed1 effect] -
不一定要像上面分析的一样,层层询问 dep 到最底层的一个,中间有可能通过 version 比对,提前就能退出, version 是实现 computed 懒计算的一个重要方案。
if (computed.globalVersion === globalVersion) { return; }简单分析一下 version 的作用
-
computed 追踪一个 globalVersion,每次
refreshComputed的时候,把自己的 globalVersion 更新为全局 globalVersion。全局的 globalVersion 在所有普通 dep 之间共享,任何一个普通 dep 只要有变化,全局 globalVersion++ ,如果computed.globalVersion === globalVersion,说明普通 dep 没有变化,computed 肯定也不会有变化 -
所有 dep 自己有一个 version,包括普通 dep 和 computed dep ,所有 link 上也有一个 version,link 上的 version 记录的是这个 link 对应 effect 最后一次读取 这个 link 对应 dep 的 version,如果两个不同,那说明 dep 更新,所以 effect 应该执行
effect(() => { console.log("effect 1") a.value }) debugger obj.a++简化版关系,link 链接 dep 和 effect,当上面代码 effect 里的回调函数执行后,link 和 obj.a dep 的 version 号都会同步一直,当 obj.a++ ,obj.a dep的版本号和 link 上就不同了
graph LR Node1[link version=1] --> Node2[obj.a dep version=1] Node1[link version=1] --> Node3[effect1]现在换成 computed 理解,effect1 执行后,各种 version 版本号都相同,obj.a++ 后,最终造成
computed1 effect更新,更新后发现值相同,computed1 dep version不变,computed2 effect对应 link (computed2 effect link)上version 记录是computed1 dep上一次的 version,由于没变,所以computed2 effect不会执行const computed1 = computed(() => { console.log("computed 1") return obj.a * 0 }) const computed2 = computed(() => { console.log("computed 2") return computed1.value + obj.c }) effect(() => { console.log("effect 1") computed2.value }) debugger obj.a++link 上 直接记录 dep 的值更直观,新旧值比对就知道要不要更新,但是用 version 记录更省内存
-