Skip to content

3.4版本响应式

结合讲述3.4源码依赖收集的过程,以及在computed中,当依赖的属性(比如当依赖count1变成依赖count2)发生变化,3.4版本是如何清楚无效依赖的

在vue3到3.4版本中,响应式一直都是有两个核心类,Dep, effect也称为Sub 组成的,常见的 Depref, reactive, computed,常见的 effect 有computed, watch, watchEffect, render函数

依赖收集过程

我们知道,在vue2或者vue3中定义的响应式数据都是通过Object.defineProperty或者Proxy实现,不管是哪种方法,都是通过属性拦截,在读取属性的时候,记录一下当前正在执行的effect是谁, 当这个属性发生改变的时候,我们把这个记录的effect拿出来执行一下, 而这个记录操作,正是通过Dep实现的,我们给每一个属性都挂一个dep,dep记录我在哪个effect执行过即可,具体看下ref的实现 下面都只展示最核心代码,方便理解

ts
class RefImpl<T> {
  // 给ref对象一个dep属性
  public dep?: Dep = undefined
  private _value: T
  ...

  constructor(
    value: T,
  ) {
    this._value = value
  }

  // 当在render函数或者computed中以及其它effect中访问ref时,会触发这个方法
  // 此写法相当于Object.defineProperty(obj, {get...})
  get value() {
    trackRefValue(this)
    return this._value
  }
}

trackRefValue实现

ts
export function trackRefValue(ref: RefBase<any>) {
  trackEffect(
    activeEffect,
    // 初始化ref上的dep,如果ref.dep没有的话
    (ref.dep ??= createDep()),
  )
}

trackEffect实现

ts
export function trackEffect(
  effect: ReactiveEffect,
  dep: Dep,
) {
  dep.set(effect, effect._trackId)
  effect.deps[effect._depsLength++] = dep
}

可以看到,当我们访问 ref.value 的时候,会被RefImpl类中value方法所拦截,从而执行trackRefValue 而在执行trackRefValue,会给当前的ref对象挂一个dep属性, 用来做依赖收集,看下dep对象长什么样子

ts
export const createDep = (
  cleanup: () => void,
  computed?: ComputedRefImpl<any>,
): Dep => {
  const dep = new Map() as Dep
  return dep
}

dep实际上就是一个map对象,为什么是map? 因为一个属性,我们既可以在render函数中使用,也可以在computed中使用,而render函数computed它们属于effect,同样,一个属性可以在A组件B组件...中使用,而一个组件对应一个render函数,一个render函数中也可以有很多个属性,因此depeffect属于多对多关系。

接下来我们真正执行依赖收集的函数trackEffect,核心就是下面两行代码

ts
...
dep.set(effect, effect._trackId)
...
effect.deps[effect._depsLength++]

我们把depactiveEffect互相关联了起来,而activeEffect,可以理解为当前正在执行的effect

举个例子

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app"></div>
  <script type="module">
    import { createApp, computed, ref } from './vue.runtime.esm-bundler.js'
    
    const app = createApp({
      template: `
        <p>{{ val }}</p>
        <button @click="changeVal">改变val</button>
      `,
      setup() {
        const val = ref(1)
        const changeVal = () => {
          val.value = Math.random()
        }
        return {
          val,
          changeVal,
        }
      }
    })
    app.mount('#app')
  </script>
</body>
</html>

当执行render函数的时候,此时这个activeEffect可以理解为我们的render函数,由于在render函数中访问ref对象,会自动.value,因此我们会进入ref的get value方法,命中了上面依赖收集的流程,dep收集了render effect, render effect记住了我依赖了哪些dep, 当修改value的时候,会把ref上的dep拿出来,看看自己上面有哪些effect,都拿出来执行一遍,这就是依赖收集的过程。下面为简化后的源码,方便理解

ts
set value(newVal) {
  // 执行triggerRefValue方法
  triggerRefValue(this, newVal, oldVal)
}

export function triggerRefValue(
  ref: RefBase<any>,
  newVal?: any,
  oldVal?: any,
) {
  // 拿出ref上的dep
  const dep = ref.dep
  if (dep) {
    triggerEffects(
      dep
    )
  }
}

export function triggerEffects(
  dep: Dep
) {
  // 遍历dep收集的effect,挨个拿出来执行
  for (const effect of dep.keys()) {
    effect.trigger()
  }
}

清除不用的依赖

以下面这个例子举例,当点击按钮时,doubleCount 的依赖从 flag, count1 变成了 flag, count2, 此时的 count1 就变成了无效依赖,3.4版本是如何解决的?

html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app"></div>
  <script type="module">
    import { createApp, computed, ref } from './vue.runtime.esm-bundler.js'
    
    const app = createApp({
      template: `
        <p>{{ doubleCount }}</p>
        <button @click="changeFlag">切换flag</button>
      `,
      setup() {
        const count1 = ref(1)
        const count2 = ref(10)
        const flag = ref(true)

        const doubleCount = computed(() => {
          console.log('computed');
          if(flag.value) {
            return count1.value * 2
          } else {
            return count2.value * 2
          }
        })

        const changeFlag = () => {
          flag.value = !flag.value
        }

        return {
          count1,
          count2,
          flag,
          doubleCount,
          changeFlag,
        }
      }
    })
    app.mount('#app')
  </script>
</body>
</html>

当修改flag之后,会触发 set value 的拦截,里面会执行triggerRefValue方法,记住,此时的flag上的dep属性收集的依赖是 computed effect

ts
set value(newVal) {
  const useDirectValue =
    this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
  newVal = useDirectValue ? newVal : toRaw(newVal)
  if (hasChanged(newVal, this._rawValue)) {
    const oldVal = this._rawValue
    this._rawValue = newVal
    this._value = useDirectValue ? newVal : toReactive(newVal)
    // 执行 triggerRefValue方法
    triggerRefValue(this, DirtyLevels.Dirty, newVal, oldVal)
  }
}

export function triggerRefValue(
  ref: RefBase<any>,
  dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
  newVal?: any,
  oldVal?: any,
) {
  ref = toRaw(ref)
  // 取出 computed 上的依赖收集器 dep, 在我们这个例子中, dep收集的是 computed effect
  const dep = ref.dep
  if (dep) {
    triggerEffects(
      dep,
      dirtyLevel,
    )
  }
}

export function triggerEffects(
  dep: Dep,
  dirtyLevel: DirtyLevels,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  pauseScheduling()
  for (const effect of dep.keys()) {
    // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
    let tracking: boolean | undefined
    if (
      effect._dirtyLevel < dirtyLevel &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
      effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
      // 把该 effect 变为脏的,因为 例子中 computed 依赖的属性发生了变化
      // 把收集到的 effect 变为脏的,代表该 effect 待会一定要执行
      effect._dirtyLevel = dirtyLevel
    }
    if (
      effect._shouldSchedule &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
      if (__DEV__) {
        // eslint-disable-next-line no-restricted-syntax
        effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
      }
      // 执行 computed effect 上的 trigger, 待会讲
      effect.trigger()
      if (
        (!effect._runnings || effect.allowRecurse) &&
        effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
      ) {
        effect._shouldSchedule = false

        // computed effect 没有 scheduler 因此不会放到调度队列中
        if (effect.scheduler) {
          queueEffectSchedulers.push(effect.scheduler)
        }
      }
    }
  }
  resetScheduling()
}

export enum DirtyLevels {
  NotDirty = 0, // 不脏
  QueryingDirty = 1,  // 查询脏不脏中
  MaybeDirty_ComputedSideEffect = 2,  // 还没发现这个有啥用
  MaybeDirty = 3, // 可能是脏的 
  Dirty = 4,  // 脏
}

可以看到,代码执行到了 effect.trigger(), 我们看下computed上的 effect 是长什么样子的

ts
export class ComputedRefImpl<T> {
  ...
  // effect 是 ReactiveEffect 的一个实例
  this.effect = new ReactiveEffect(
    () => getter(this._value),
    () =>
      triggerRefValue(
        this,
        this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
          ? DirtyLevels.MaybeDirty_ComputedSideEffect
          : DirtyLevels.MaybeDirty,
      ),
  )
  // 代表该 effect 是 computed effect
  this.effect.computed = this
  ...
}

// ReactvieEffect 是长这个样子的

export class ReactiveEffect<T = any> {
  deps: Dep[] = []

  // 是否是 computed effect的标识
  computed?: ComputedRefImpl<T>

  // 初始值是脏的,因为初始化的时候,如果用到了,肯定要执行一下 effect.run()
  _dirtyLevel = DirtyLevels.Dirty
  // 该effect依赖了几个dep
  _depsLength = 0

  constructor(
    public fn: () => T, // fn 可以理解为传给 computed 的 getter函数 computed(() => {...}),此时就是 () => {...}
    public trigger: () => void, // 依赖的dep发生变化,执行该函数
    public scheduler?: EffectScheduler,
  ) {
  }

  run() {
    // run 过了, 已经不脏了
    this._dirtyLevel = DirtyLevels.NotDirty
    let lastShouldTrack = shouldTrack
    let lastEffect = activeEffect
    try {
      shouldTrack = true
      // 代表当前正在执行的 effect 是我,this.fn 里面访问到的变量都要收集我自己
      activeEffect = this
      this._runnings++
      preCleanupEffect(this)
      return this.fn()
    } finally {
      postCleanupEffect(this)
      this._runnings--
      // 还原 activeEffect 
      activeEffect = lastEffect
      shouldTrack = lastShouldTrack
    }
  }

}

可以看到 effect.trigger() 就是下面这段逻辑

ts
triggerRefValue(
  this, // 传进去了 computed, 而此时 computed dep 上收集的是 render effect
  this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
    ? DirtyLevels.MaybeDirty_ComputedSideEffect
    : DirtyLevels.MaybeDirty,
),

export function triggerRefValue(
  ref: RefBase<any>,
  dirtyLevel: DirtyLevels = DirtyLevels.Dirty, // 这次传进来的是3 可能是脏的
  newVal?: any,
  oldVal?: any,
) {
  ref = toRaw(ref)
  const dep = ref.dep
  if (dep) {
    triggerEffects(
      dep,
      dirtyLevel
    )
  }
}

export function triggerEffects(
  dep: Dep,
  dirtyLevel: DirtyLevels,  // 传进来的是 3
) {
  pauseScheduling()
  // 此时 computed dep 上只有 render effect
  for (const effect of dep.keys()) {
    let tracking: boolean | undefined
    if (
      effect._dirtyLevel < dirtyLevel &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
      effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
      effect._dirtyLevel = dirtyLevel // 3 可能是脏的
    }
    if (
      effect._shouldSchedule &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
      effect.trigger()  // render effect 的 trigger 是一个空函数,因为更新是异步的
      if (
        (!effect._runnings || effect.allowRecurse) &&
        effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
      ) {
        effect._shouldSchedule = false
        // 更新逻辑放在 调度队列里,异步执行
        if (effect.scheduler) {
          queueEffectSchedulers.push(effect.scheduler)
        }
      }
    }
  }
  resetScheduling()
}

接下来异步执行 effect.scheduler,可以理解为重新执行下 render effect.run,就会开启新一轮的依赖收集流程 但是在执行的时候,我们需要判断 render effect 是不是脏的,如果不脏,完全可以不更新 在上面的流程中 render effect._dirtyLevel 被标记为 3, 代表可能是脏的,还需要进一步判断脏不脏

ts
const update: SchedulerJob = (instance.update = () => {
  // 执行 render effect run之前需要判断脏不脏
  if (effect.dirty) {
    effect.run()
  }
})

// 访问 effect.dirty属性会被拦截,判断脏不脏
public get dirty() {
  if (
    this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
    // 此时为3 DirtyLevels.MaybeDirty 命中 if 逻辑
    this._dirtyLevel === DirtyLevels.MaybeDirty
  ) {
    // 标记正在查询是不是脏的
    this._dirtyLevel = DirtyLevels.QueryingDirty
    pauseTracking()
    // 遍历 render effect 上所有的 dep, 找出 computed dep
    for (let i = 0; i < this._depsLength; i++) {
      const dep = this.deps[i]
      // 找出 computed dep
      if (dep.computed) {
        triggerComputed(dep.computed) // 核心是这一句,下面有代码
        if (this._dirtyLevel >= DirtyLevels.Dirty) {
          break
        }
      }
    }
    if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
      this._dirtyLevel = DirtyLevels.NotDirty
    }
    resetTracking()
  }
  return this._dirtyLevel >= DirtyLevels.Dirty
}

function triggerComputed(computed: ComputedRefImpl<any>) {
  // 重新访问 computed.value
  return computed.value
}

// 访问 value 会被拦截
get value() {
  const self = toRaw(this)
  if (
    // 判断 comouted 的 effect 是不是脏的
    // 很明显,在上面的流程中,computed effect._dirtyLevel为4,已经脏了
    // 会重新执行计算属性的 effect.run,就是重新执行传给computed的函数
    (!self._cacheable || self.effect.dirty) &&
    hasChanged(self._value, (self._value = self.effect.run()!))
  ) {
    triggerRefValue(self, DirtyLevels.Dirty)
  }
  trackRefValue(self)
  if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
    if (__DEV__ && (__TEST__ || this._warnRecursive)) {
      warn(COMPUTED_SIDE_EFFECT_WARN, `\n\ngetter: `, this.getter)
    }
    triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
  }
  return self._value
}

// effect.run代码
run() {
  // run 过就不脏了,computed effect在这里被标记为不脏
  this._dirtyLevel = DirtyLevels.NotDirty
  let lastShouldTrack = shouldTrack
  let lastEffect = activeEffect
  try {
    shouldTrack = true
    activeEffect = this
    preCleanupEffect(this)  // 核心就是 effect._depsLength = 0,方便重新计算依赖
    // 执行依赖收集,执行computed传进去的函数,里面访问到的响应式属性,如 ref,都会被依赖收集
    return this.fn()
  } finally {
    postCleanupEffect(this)
    activeEffect = lastEffect
    shouldTrack = lastShouldTrack
  }
}

function preCleanupEffect(effect: ReactiveEffect) {
  effect._depsLength = 0
}

代码走到了这里,就会去重新执行 computed effect的依赖收集流程,还记得computed effect 的依赖从flag 和 count1变为 flag 和 count2吗? 下面就是3.4版本的变换流程

当我们再一次访问到 flag的时候,由于上次我们已经收集过flag了,

ts
// 访问 flag1.value 会走到这里
export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    trackEffect(
      activeEffect,
      // 上次收集过,所以 dep 不用重新初始化
      (ref.dep ??= createDep(
        () => (ref.dep = undefined),
        ref instanceof ComputedRefImpl ? ref : undefined,
      ))
    )
  }
}

export function trackEffect(
  effect: ReactiveEffect,
  dep: Dep,
) {
  // dep 为 flag 的 dep,新一轮 run了,effect._trackId肯定和之前不一样了
  if (dep.get(effect) !== effect._trackId) {
    // 放进dep
    dep.set(effect, effect._trackId)
    // 取出之前该位置所对应的dep
    // 什么叫该位置,就是在computed传进去的函数中,flag 是第一个访问的,该位置就是1
    // 重新执行传进去的函数,flag 还是第1个访问的,没问题,复用dep即可 !! 核心复用流程
    const oldDep = effect.deps[effect._depsLength]
    // 都是 flag 的 dep,复用
    if (oldDep !== dep) {
      if (oldDep) {
        cleanupDepEffect(oldDep, effect)
      }
      effect.deps[effect._depsLength++] = dep
    } else {
      // 复用的话直接让 effect._depsLength++即可
      effect._depsLength++
    }
  }
}

当我们访问count2的时候,也会执行依赖收集

ts
// 此时 ref是 count2
export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    trackEffect(
      activeEffect,
      // count2 之前没有被收集过,需要给 count2 初始化 dep
      (ref.dep ??= createDep(
        () => (ref.dep = undefined),
        ref instanceof ComputedRefImpl ? ref : undefined,
      ))
    )
  }
}

export function trackEffect(
  effect: ReactiveEffect,
  dep: Dep, // count2
) {
  if (dep.get(effect) !== effect._trackId) {
    // count2 的 dep收集 该effect
    dep.set(effect, effect._trackId)
    // 访问之前的位置,之前的位置是 count1
    const oldDep = effect.deps[effect._depsLength]
    // 现在变成 count2 了,肯定不相等
    if (oldDep !== dep) {
      if (oldDep) {
        // 清理旧的依赖
        cleanupDepEffect(oldDep, effect)
      }
      // 让 effect.deps对应的位置由 count1 变为 count2
      // 至此,dep 和 effect都完成了各自的清理工作
      effect.deps[effect._depsLength++] = dep
    } else {
      effect._depsLength++
    }
  }
}

function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
  const trackId = dep.get(effect)
  if (trackId !== undefined && effect._trackId !== trackId) {
    // count1的dep删除掉effect
    dep.delete(effect)
    ...
  }
}

有没有发现一个问题,我们从判断render effect脏不脏,搞了半天没有看到对应的逻辑,反而看到了依赖清理的流程 其实,依赖清理逻辑只是判断render effect脏不脏其中的一小个环节而已, 我们继续判断render effect脏不脏,我们刚刚到了 get value,再回顾下

ts
const update: SchedulerJob = (instance.update = () => {
  // 执行 render effect前判断 render effect 脏不脏,不脏则不重新渲染
  if (effect.dirty) {
    effect.run()
  }
})

// 访问 effect.dirty属性会被拦截,判断脏不脏
public get dirty() {
  if (
    this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
    // 此时为3 DirtyLevels.MaybeDirty 命中 if 逻辑
    this._dirtyLevel === DirtyLevels.MaybeDirty
  ) {
    // 标记正在查询是不是脏的
    this._dirtyLevel = DirtyLevels.QueryingDirty
    pauseTracking()
    // 遍历 render effect 上所有的 dep, 找出 computed dep
    for (let i = 0; i < this._depsLength; i++) {
      const dep = this.deps[i]
      // 找出 computed dep
      if (dep.computed) {
        triggerComputed(dep.computed) // 核心是这一句,下面有代码
        if (this._dirtyLevel >= DirtyLevels.Dirty) {
          break
        }
      }
    }
    if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
      this._dirtyLevel = DirtyLevels.NotDirty
    }
    resetTracking()
  }
  return this._dirtyLevel >= DirtyLevels.Dirty
}

function triggerComputed(computed: ComputedRefImpl<any>) {
  return computed.value
}

get value() {
  const self = toRaw(this)
  if (
    (!self._cacheable || self.effect.dirty) &&
    // 上面一大坨依赖清理流程,就是在这个self.effect.run()中执行的
    hasChanged(self._value, (self._value = self.effect.run()!))
  ) {
    // 此时computed计算的值,跟之前的值不一样了,代表 render effect脏了
    triggerRefValue(self, DirtyLevels.Dirty)
  }
  trackRefValue(self)
  return self._value
}

export function triggerEffects(
  dep: Dep,
  dirtyLevel: DirtyLevels, // 传进来是4
) {
  pauseScheduling()
  for (const effect of dep.keys()) {
    // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
    let tracking: boolean | undefined
    if (
      effect._dirtyLevel < dirtyLevel &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
      effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
      effect._dirtyLevel = dirtyLevel // render effect 变脏了
    }
    ...
  }
  resetScheduling()
}

到了上面,render effect已经变脏了 triggerComputed流程执行结束,回到dirty属性的拦截中来,继续往下走

ts
public get dirty() {
  if (
    this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
    this._dirtyLevel === DirtyLevels.MaybeDirty
  ) {
    this._dirtyLevel = DirtyLevels.QueryingDirty
    pauseTracking()
    for (let i = 0; i < this._depsLength; i++) {
      const dep = this.deps[i]
      if (dep.computed) {
        // 这里就是把 render effect 变脏的地方
        triggerComputed(dep.computed)
        // 已经脏了,break
        if (this._dirtyLevel >= DirtyLevels.Dirty) {
          break
        }
      }
    }
    // 脏了,而不是正在查询脏不脏的状态了
    if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
      this._dirtyLevel = DirtyLevels.NotDirty
    }
    resetTracking()
  }
  // 脏了,返回true
  return this._dirtyLevel >= DirtyLevels.Dirty
}

const update: SchedulerJob = (instance.update = () => {
  // 脏了,要重新渲染
  if (effect.dirty) {
    // 渲染
    effect.run()
  }
})

至此,依赖收集流程,依赖改变触发更新,清理无效依赖流程已经结束 那么有一个问题,为什么要判断脏不脏呢?因为虽然计算属性的依赖发生了变化,但是最后计算出来的值可能是不变的,页面依然可以不刷新。 我们只需要改变computed effect收集的dep,清理掉无效依赖即可。

给自己看的总结

在最后判断脏不脏中,是拿 render effect上所有的computed dep,执行一下computed.value计算新的值,此时会对computed effect进行新一轮的 依赖收集,以及清除无效依赖,(计算完后computed effect已经不脏了)。在render effect中执行的时候,computed effect已经不脏,不需要重新计算 和依赖收集,仅仅让computed的dep收集render effect即可。