零到一打造 Vue3 响应式系统 Day 15 - Effect:依赖清理实现方案

368 阅读5分钟

ZuB1M1H.png

在实际情况中,effect 函数内部的依赖,常常因为条件分支(比如 if...else)而发生变化,这种情况称为「动态依赖」。

动态依赖会带来一个问题:在某次执行中不再被使用的旧依赖,如果没有被处理好,就会残留在依赖列表中。

后续当这个失效依赖的来源被修改时,仍然会触发 effect 重新执行,导致不必要的更新或逻辑错误。

前情回顾

day15-01.png

  • 第一次执行flag.valuetrueeffect 依赖 flagname,系统建立依赖链表 link1(flag) -> link2(name)
  • 触发更新flag.value 变为 falseeffect 重新执行。
  • 第二次执行effect 进入 else 分支,需要依赖 age。系统会复用 link1(flag),并且为 age 建立新节点 link3(age)。在没有清理机制时,旧的 link2(name) 依然存在于 effect 的依赖链表中。

此时,如果修改 name.value,因为 link2(name) 的依赖关系还在,effect 会再次被触发,而当前 effect 的输出内容实际上只与 age 有关。

依赖清理核心思路

最直接的方法是在每次 effect 执行前,清空所有依赖再重新收集,但这样会导致无法复用已有的链表节点,性能较差。

另一种更高效的方法是,在执行结束后,找出本次没有访问到的节点,并只清除那部分。

场景一:条件性依赖

在这里可以做判断,因为 effectname 切换到 age 后,depsTail 的最后位置会指向 link3

day15-02.png

当执行完毕后,depsTail 指向 link3,而 link3 存有一个 nextDep 指针,指向旧的 link2(name)。这就提供了一个判断依据:

「从 depsTail 节点的 nextDep 开始,到链表末尾的所有节点,都是本次执行时没有访问到的依赖」

以当前案例来说:

  1. depsTail 指向 link3
  2. link3 此时仍然有 nextDep

就可以清理 link3nextDep,依赖清理完成。

情况二:提前返回

day15-03.png

还记得上次我们一直触发按钮,链表的状态保持在:有头节点 deps,但是尾节点 depsTail = undefined

如果 effect 执行时因为条件判断提前 return,没有访问任何响应式数据。depsTail 会保持初始 undefined 状态。

这时就有了另一种判断依据:

「当 effect 执行完毕后,如果 depsTailundefined 并且 deps 头节点存在,就说明本次执行没有访问任何依赖,应该清除所有旧依赖」

代码实现依赖清理

我们使用 startTrackendTrack 两个函数来管理 effect 的执行周期。

  1. depsTail 存在,并且 depsTailnextDep 存在,表示包含 nextDep 的后续链表节点应该被移除,传入 clearTracking 函数。
  2. 如果触发更新完全没有读取到任何依赖(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 设计核心

day15-04.png

clearTracking 函数的作用是从链表中移除一个 link 节点。

day15-05.png

由于 link 节点同时存在于 dep 的订阅者列表 (dep.subs) 和 effect 的依赖列表 (effect.deps) 这两个双向链表中,移除操作需要更新其在 dep.subs 列表中的 prevSubnextSub 指针,然后再沿着 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
}

执行结果

day15-06.gif

失效依赖是在实现响应式系统时必须处理的问题。这次我们利用 deps 链表和 depsTail 指针,在 effect 执行完毕后,可以确认并移除不再使用的依赖项。


想了解更多 Vue 的相关知识,抖音、B站搜索我师父「远方os」,一起跟日安当同学。