在实际情况中,effect 函数内部的依赖,常常因为条件分支(比如 if...else)而发生变化,这种情况称为「动态依赖」。
动态依赖会带来一个问题:在某次执行中不再被使用的旧依赖,如果没有被处理好,就会残留在依赖列表中。
后续当这个失效依赖的来源被修改时,仍然会触发 effect 重新执行,导致不必要的更新或逻辑错误。
前情回顾
- 第一次执行:
flag.value为true,effect依赖flag和name,系统建立依赖链表link1(flag) -> link2(name)。 - 触发更新:
flag.value变为false,effect重新执行。 - 第二次执行:
effect进入else分支,需要依赖age。系统会复用link1(flag),并且为age建立新节点link3(age)。在没有清理机制时,旧的link2(name)依然存在于effect的依赖链表中。
此时,如果修改 name.value,因为 link2(name) 的依赖关系还在,effect 会再次被触发,而当前 effect 的输出内容实际上只与 age 有关。
依赖清理核心思路
最直接的方法是在每次 effect 执行前,清空所有依赖再重新收集,但这样会导致无法复用已有的链表节点,性能较差。
另一种更高效的方法是,在执行结束后,找出本次没有访问到的节点,并只清除那部分。
场景一:条件性依赖
在这里可以做判断,因为 effect 从 name 切换到 age 后,depsTail 的最后位置会指向 link3。
当执行完毕后,depsTail 指向 link3,而 link3 存有一个 nextDep 指针,指向旧的 link2(name)。这就提供了一个判断依据:
「从 depsTail 节点的 nextDep 开始,到链表末尾的所有节点,都是本次执行时没有访问到的依赖」
以当前案例来说:
depsTail指向link3link3此时仍然有nextDep
就可以清理 link3 的 nextDep,依赖清理完成。
情况二:提前返回
还记得上次我们一直触发按钮,链表的状态保持在:有头节点 deps,但是尾节点 depsTail = undefined。
如果 effect 执行时因为条件判断提前 return,没有访问任何响应式数据。depsTail 会保持初始 undefined 状态。
这时就有了另一种判断依据:
「当 effect 执行完毕后,如果 depsTail 是 undefined 并且 deps 头节点存在,就说明本次执行没有访问任何依赖,应该清除所有旧依赖」
代码实现依赖清理
我们使用 startTrack 和 endTrack 两个函数来管理 effect 的执行周期。
depsTail存在,并且depsTail的nextDep存在,表示包含nextDep的后续链表节点应该被移除,传入clearTracking函数。- 如果触发更新完全没有读取到任何依赖(
depsTail=undefined,但有sub.deps头节点),此时也应该移除,传入clearTracking函数。
//effect.ts
...
...
export class ReactiveEffect {
...
run(){
...
}finally{
endTrack(this)
activeSub = prevSub
}
}
...
}
function endTrack(sub){
const depsTail = sub.depsTail
/**
*
* 情况一解法: depsTail 存在,并且 depsTail 的 nextDep 存在,表示后续链表节点应该移除
*/
if(depsTail){
if(depsTail.nextDep){
clearTracking(depsTail.nextDep)
depsTail.nextDep = undefined
}
// 情况二:depsTail 不存在,但旧的 deps 头节点存在,清除所有节点
}else if(sub.deps){
clearTracking(sub.deps)
sub.deps = undefined
}
}
clearTracking 设计核心
clearTracking 函数的作用是从链表中移除一个 link 节点。
由于 link 节点同时存在于 dep 的订阅者列表 (dep.subs) 和 effect 的依赖列表 (effect.deps) 这两个双向链表中,移除操作需要更新其在 dep.subs 列表中的 prevSub 和 nextSub 指针,然后再沿着 effect.deps 列表的 nextDep 指针继续处理下一个待清理的节点。
clearTracking 实现
/**
* 清理依赖函数链表
*/
function clearTracking(link: Link){
while(link){
const { prevSub, nextSub, dep, nextDep} = link
/**
* 1. 如果上一个节点存在 sub,就把它的 nextSub 指向当前节点的下一个节点
* 2. 如果没有 sub,表示是头节点,那就把 dep.subs 指向当前节点的下一个节点
*/
if(prevSub){
prevSub.nextSub = nextSub
link.nextSub = undefined
}else{
dep.subs = nextSub
}
/**
* 1. 如果下一个节点存在 sub,就把它的 prevSub 指向当前节点的上一个节点
* 2. 如果没有 sub,表示是尾节点,那就把 dep.subsTail 指向当前节点的上一个节点
*/
if(nextSub){
nextSub.prevSub = prevSub
link.prevSub = undefined
}else{
dep.subsTail = prevSub
}
link.dep = link.sub = undefined
link.nextDep = undefined
link = nextDep
}
}
...
...
system.ts 调整
export function link(dep, sub){
/**
* 复用节点
* sub.depsTail 是 undefined,并且有 sub.deps 头节点,表示要复用
*/
const currentDep = sub.depsTail // = link1
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
// nextDep = link1.nextDep = link2
if(nextDep && nextDep.dep === dep){
// link2.dep (name) === age ? → false! 不能复用,需要新建 link
sub.depsTail = nextDep
return
}
const newLink = {
sub,
dep,
nextDep, // 让 link3 的 nextDep 指向 link2
nextSub:undefined,
prevSub:undefined
}
if(dep.subsTail){
dep.subsTail.nextSub = newLink
newLink.prevSub = dep.subsTail
dep.subsTail = newLink
}else {
dep.subs = newLink
dep.subsTail = newLink
}
if(sub.depsTail){
sub.depsTail.nextDep = newLink
sub.depsTail = newLink
}else{
sub.deps = newLink
sub.depsTail = newLink
}
}
重构调整:完整代码
system.ts
// system.ts
// ... (接口定义不变)
export function link(dep, sub){
const currentDep = sub.depsTail
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
if (nextDep && nextDep.dep === dep) {
sub.depsTail = nextDep
return
}
const newLink: Link = {
sub,
dep,
nextDep,
nextSub: undefined,
prevSub: undefined
}
if (dep.subsTail) {
dep.subsTail.nextSub = newLink
newLink.prevSub = dep.subsTail
dep.subsTail = newLink
} else {
dep.subs = newLink
dep.subsTail = newLink
}
if (sub.depsTail) {
sub.depsTail.nextDep = newLink
sub.depsTail = newLink
} else {
sub.deps = newLink
sub.depsTail = newLink
}
}
export function propagate(subs){
// ... (不变)
}
/**
* 开始追踪,将 depsTail 设为 undefined
*/
export function startTrack(sub){
sub.depsTail = undefined
}
/**
* 结束追踪,找到需要清理的依赖
*/
export function endTrack(sub){
const depsTail = sub.depsTail
if (depsTail) {
if (depsTail.nextDep) {
clearTracking(depsTail.nextDep)
depsTail.nextDep = undefined
}
} else if (sub.deps) {
clearTracking(sub.deps)
sub.deps = undefined
}
}
/**
* 清理依赖函数链表
*/
function clearTracking(link: Link){
while(link){
const { prevSub, nextSub, dep, nextDep} = link
if (prevSub) {
prevSub.nextSub = nextSub
link.nextSub = undefined
} else {
dep.subs = nextSub
}
if (nextSub) {
nextSub.prevSub = prevSub
link.prevSub = undefined
} else {
dep.subsTail = prevSub
}
link.dep = undefined
link.sub = undefined
link.nextDep = undefined
link = nextDep
}
}
effect.ts
// effect.ts
import { Link, startTrack, endTrack } from './system'
export let activeSub;
export class ReactiveEffect {
deps: Link
depsTail: Link
constructor(public fn){}
run(){
const prevSub = activeSub
activeSub = this
startTrack(this)
try {
return this.fn()
} finally {
endTrack(this)
activeSub = prevSub
}
}
notify(){
this.scheduler()
}
scheduler(){
this.run()
}
}
export function effect(fn, options){
const e = new ReactiveEffect(fn)
Object.assign(e, options)
e.run()
const runner = e.run.bind(e)
runner.effect = e
return runner
}
执行结果
失效依赖是在实现响应式系统时必须处理的问题。这次我们利用 deps 链表和 depsTail 指针,在 effect 执行完毕后,可以确认并移除不再使用的依赖项。
想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。