在 Vue3 的响应式系统中,有两个重要的角色——depsLength 和 trackId

响应式系统的基本原理

  • 响应式对象:通过 reactive() 或 ref() 创建
  • 副作用: effect() 在依赖的响应式数据变化时,重新执行
  • 依赖收集:在副作用执行时,会访问响应式数据并建立依赖关系

depsLength 与 trackId

trackId:周期身份号码

trackId 是一个自增的唯一标识,用来确定副作用的一次特定运行周期。这样的话,每次执行副作用都会是一个新的身份号码

class ReactiveEffect {
  private _trackId = 0
  ...
  run() {
    // 每次运行都++,每次都是不一样的
    this._trackId++
    // ...执行副作用函数
  }
}

depsLength:依赖数组的真实数量

depsLength 用于记录当前副作用实际使用的依赖数量,也就是deps中真正需要使用的数量

class ReactiveEffect {
  deps = []    // 所有依赖的集合
  depsLength = 0      // 当前有效依赖的数量
}

深入工作机制

依赖收集的完整流程

例子:

const state = reactive({ a: 1, b: 2, c: 3, d: 4, e: 5 })

// 第一次执行:依赖所有5个属性
effect(() => {
  console.log(state.a, state.b, state.c, state.d, state.e)
})

第一次运行后的状态: deps收集了5个依赖,depsLength++了5次所以是5,trackId第一次执行因为effect是第一次运行

  • effect.deps = [dep_a, dep_b, dep_c, dep_d, dep_e]
  • effect.depsLength = 5
  • effect.trackId = 1

现在条件发生变化,副作用只需要部分依赖:

// 条件改变,第二次运行:只依赖a和b
effect(() => {
  if (true) {
    console.log(state.a, state.b) // 只依赖a, b
  } else {
    console.log(state.c, state.d, state.e)
  }
})

现在开始三个阶段的工作

阶段一:预清理(Pre-Cleanup)

在副作用重新执行前,Vue 会进行预清理:

function preCleanupEffect(effect: ReactiveEffect) {
  effect.depsLength = 0  // 重置水位线,从第一个开始
  effect.trackId++       // 换发新身份证
}

此时状态变为:

  • effect.deps = [dep_a, dep_b, dep_c, dep_d, dep_e] (保持不变)
  • effect.depsLength = 0 (重置)
  • effect.trackId = 2 (更新)
阶段二:依赖收集(Dependency Tracking)

清理后开始进行依赖收集

function trackEffects(effect,dep) {
  //trackId不一样说明周期不一样,不一样就set进去
  //现在是第二次,trackId为2,而dep.get(effect)<上一次的trackId>是1
  if (dep.get(effect) !== effect._trackId) {
        dep.set(effect, effect._trackId) 
    }
    //拿到effect中的现在下标的dep,也就是老dep
    let oldDep = effect.deps[effect._depsLenght]
    //老dep与新dep不相同,说明是新添加的依赖、
    //如果是第一次oldDep就是undefined
    if (oldDep !== dep) {
        //因为老的变了,自然要把老的给清理掉,留着也没用
        if(oldDep){
            cleanDepEffect(oldDep, effect)
        }
        //无论老的有没有,只要是不同,就要把新的dep添加进去
        effect.deps[effect._depsLenght++] = dep;
    }else{
        //这里是复用的情况,++下次进行判断下一个
        effect._depsLenght++;
    }
}
function cleanDepEffect(oldDep, dep) {
    //从旧的dep中删除effect
    dep.delete(oldDep)
    //如果dep已经没有effect了,就执行cleanup函数,从映射表中删除该key对应的dep
    if(dep.size === 0){
        dep.cleanup()
    }
}

在我们的例子中:

  1. 访问 state.a → 复用 deps[0] 存放 dep_adepsLength = 1
  2. 访问 state.b → 复用 deps[1] 存放 dep_bdepsLength = 2

此时状态:

  • effect.deps = [dep_a, dep_b, dep_c, dep_d, dep_e] (前两个被复用,但c,d,e还在,它们已经是多余的了要删掉)
  • effect.depsLength = 2
  • effect.trackId = 2
阶段三:最终清理(Final Cleanup)

清理多余的依赖:

function finalizeDepMarkers(effect: ReactiveEffect) {
  const { deps, depsLength } = effect
  
  //因为上一步的操作中无论是复用或者是替换都会把desLength推到最新数量的位置
  //因此如果deps[]的长度大于desLength,则都是多余的了
  if (depsLength < deps.length) {
    for (let i = depsLength; i < deps.length; i++) {
      const dep = deps[i]
      if (dep) {
        // 从依赖集合中移除这个effect
        cleanUpDepEffect(dep, effect)
        // 清空数组引用
        deps[i] = null!
      }
    }
    // 最后把deps[]的数组长度调整到最新的长度
    deps.length = depsLength
  }
}

function cleanUpDepEffect(dep: Dep, effect: ReactiveEffect) {
  dep.delete(effect) // 从dep的集合中移除effect
}

现在的操作:

  • 清理 dep_cdep_ddep_e
  • 设置 deps.length = 2

最终状态:

  • effect.deps = [dep_a, dep_b] (修改后的deps)
  • effect.depsLength = 2
  • effect.trackId = 2

总结

两者相辅相成,两个小小的number保证了响应式系统的准确和效率

  • tarckId:是周期的唯一标识,用于防止重复收集依赖
  • depsLength:记录当前的deps的位置,在清理dep上起到重要作用
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]