1. 依赖清理优化算法详解-算法思路

每次执行副作用函数前都会先清理所有依赖再重新收集,从而减少无效依赖的触发。但如下情况,这种清理工作反而增加无谓的性能消耗

const state = reactive({ show: true, values: [1,2,3] })
effect(() => {
  console.log(state.values)
})

那么应该如何处理呢?@vue/reactivity已经为我们提供了一个非常优秀的解决方案,请看下面吧。

1.1. 逐行分析优化算法

export type Dep = Set<ReactiveEffect> & TrackedMarkers

type TrackedMarkers = {
  /**
   * wasTracked的缩写,采用二进制格式,每一位表示不同effect嵌套层级中,该依赖是否已被跟踪过(即在上一轮副作用函数执行时已经被访问过)
   */ 
  w: number
  /**
   * newTracked的缩写,采用二进制格式,每一位表示不同effect嵌套层级中,该依赖是否为新增(即在本轮副作用函数执行中被访问过)
   */ 
  n: number
}

export const createDep = (effects) => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  // 虽然TrackedMarkers标识是位于响应式对象属性的依赖集合上,但它每一位仅用于表示当前执行的副作用函数是否曾经访问和正在访问该响应式对象属性
  dep.w = 0
  dep.n = 0

  return dep
}

export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0

export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0

/**
 * 将当前副作用函数的依赖标记为 `已经被收集`
 */
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit
    }
  }
}

/**
 * 用于对曾经跟踪过,但本次副作用函数执行时没有跟踪的依赖,采取删除操作。
 * 即,新跟踪的 和 本轮跟踪过的都会被保留。
 */
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
  const { deps } = effect
  if (deps.length) {
    let ptr = 0
    for (let i = 0; i < deps.length; i++) {
      const dep = deps[i]
      if (wasTracked(dep) && !newTracked(dep)) {
        // 对于曾经跟踪过,但本次副作用函数执行时没有跟踪的依赖,采取删除操作。
        dep.delete(effect)
      }
      else {
        // 缩小依赖集合的大小
        deps[ptr++] = dep
      }
      // 将w和n中对应的嵌套层级的二进制位置零,如果缺少这步后续副作用函数重新执行时则无法重新收集依赖。
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    // 缩小依赖集合的大小
    deps.length = ptr
  }
}
// 在位于响应式上下文执行的副作用函数内,访问响应式对象属性,将通过track收集依赖
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!isTracking()) {
    return
  }

  // targetMap用于存储响应式对象-对象属性的键值对
  // depsMap用于存储对象属性-副作用函数集合的键值对
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    target.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  trackEffects(dep)
}

// 收集依赖
export function trackEffects(
  dep: Dep
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    // 如果本轮副作用函数执行过程中已经访问并收集过,则不用再收集该依赖
    if (!newTracked(dep)) {
      dep.n |= trackOpBit
      shouldTrack = !wasTracked(dep)
    }
  }
  else {
    // 对于全面清理的情况,如果当前副作用函数对应的ReactiveEffect对象不在依赖集合中,则标记为true
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
  }
}

单单从代码实现角度能难理解这个优化方式,不如我们从实际的例子出发吧!

const runAsync = fn => setTimeout(fn, 1000)

const state = reactive({ show: true, values: [1,2,3] })
// 1
effect(() => {
  if (state.show) {
    console.log(state.values)
  }
})

// 2
runAsync(() => {
  state.values.push(4)
})

// 3
runAsync(() => {
  state.show = false
})
  1. 首次执行副作用函数 a. effectTrackDepth为0,因此1 << ++effectTrackDepth得到的effectTrackDepthtrackOpBit均为1,但由于此时副作用函数还没有收集依赖,因此initDepMarkers函数没有任何效果; b. 访问state.show时由于之前没有收集过响应式对象stateshow属性,因此会调用createDep创建wn均为0的依赖集合,并调用trackEffects发现newTracked(dep)为未跟踪过,则将n设置为1,然后开始收集依赖; c. 访问state.values会重复第2步的操作; d. 由于state.showstate.values都是新跟踪的(n为1),因此在finalizeDepMarkers处理后仍然将副作用函数保留在这两个属性对应的依赖集合中。
  2. 执行state.values.push(4)触发副作用函数变化 a. effectTrackDepth为0,因此1 << ++effectTrackDepth得到的effectTrackDepthtrackOpBit均为1,此时副作用函数已经收集过依赖,因此initDepMarkers将该副作用函数所在的依赖集合都都标记为已收集过(w为1); b. 访问state.show时会调用trackEffects发现newTracked(dep)为未跟踪过(在finalizeDepMarkers中已被置零),则将n设置为1,然后开始收集依赖; c. 访问state.values会重复第2步的操作; d. 由于state.showstate.values都是新跟踪的(n为1),因此在finalizeDepMarkers处理后仍然将副作用函数保留在这两个属性对应的依赖集合中。
  3. 执行state.show = false触发副作用函数变化 a. effectTrackDepth为0,因此1 << ++effectTrackDepth得到的effectTrackDepthtrackOpBit均为1,此时副作用函数已经收集过依赖,因此initDepMarkers将该副作用函数所在的依赖集合都都标记为已收集过(w为1); b. 访问state.show时会调用trackEffects发现newTracked(dep)为未跟踪过(在finalizeDepMarkers中已被置零),则将n设置为1,然后开始收集依赖; c. 由于state.values没有标记为新跟踪的(n为0),因此在finalizeDepMarkers处理后会将副作用函数从state.values对应的依赖集合中移除,仅保留在state.values对应的依赖集合中。

@vue/reactivity给我们展示了一个非常优秀的处理方式,那么就是通过标识每个依赖集合的状态(新依赖和已经被收集过),并对新依赖和已经被收集过两个标识进行对比筛选出已被删除的依赖项。

到这里,我想大家已经对这个优化有更深的理解了。那么接下来的问题自然而然就是为什么要硬编码将优化算法启动的嵌套层级设置为maxMarkerBits = 30? 请看下一节《依赖清理优化算法详解-基于JavaScript引擎的SMI进一步压榨性能》

Copyright © fsjohnhuang 2022 all right reserved,powered by GitbookFile Modify: 2022-04-26 09:47:32

results matching ""

    No results matching ""