你还不知道vue3中依赖收集和派发更新的实现逻辑吗?那你就out啦~还不快来看!

2022-09-28 15:27:56 浏览数 (1)

一、前言

本文是# 深入源码彻底搞清vue3中reactive和ref的区别的衍生篇,我们继续从源码入手,去解读vue3中的track()依赖收集以及trigger()派发更新。

在阅读本文前不知道你是否已经明白依赖收集以及派发更新的具体作用。当然不明白也没关系,本文会先语义话的讲一讲这两者的概念,这样能有一个基本的理解。接着我们在深入源码去看一看这两者的具体实现,废话不多说,进入正文(u‿ฺu✿ฺ)

二、track()依赖收集

在我们日常开发中,当我们在template中使用响应式变量,并且改变这些值时,vue总能及时的监听到这些变化并重新渲染相关的组件,那这是怎么做到的呢?

其实我们大家都知道在vue3中若要实现响应式数据需要通过reactiveref去定义我们的值(reactive和ref详情可点击)。而在reactive中则是通过get去拦截我们对数据的读取操作,在这个拦截读取的过程中我们会先将数据通过effect包裹一层然后给它收集起来,这个过程就是依赖收集

至于为什么要收集起来,是不是只有收集起来了,到时候改这个数据的时候vue是不是才能知道你改的是哪个数据,然后才能去重新渲染相对于的组件。

接着我们看看在vue3中是如何去实现这个过程的吧

effect() 副作用函数

关于effect的具体作用,大家可以看看# 基于Proxy从0到1实现响应式数据这篇文章。

源码地址:packages/reactivity/src/effect.ts

代码语言:javascript复制
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  // 如果已经是effect副作用函数,则返回副作用的原始函数
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }

  const _effect = new ReactiveEffect(fn)
  // 创建`effect`
  if (options) {
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  // 若不是延迟执行,则立即执行一次副作用函数(计算属性lazy为true)
  if (!options || !options.lazy) {
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  // 返回副作用函数
  return runner
}

这里可以看到通过new ReactiveEffect创建了一个_effect实例

代码语言:javascript复制
 const _effect = new ReactiveEffect(fn)

通过bind函数返回一个新的副作用函数runner,这个新函数的this被指定为_effect,并将_effect添加到这个新副作用函数的effect属性上,最后返回这个新副作用函数。

代码语言:javascript复制
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  // 返回副作用函数
  return runner

ReactiveEffect

我们可以看到在effect副作用函数内最终返回的runner指向ReactiveEffectrun方法

代码语言:javascript复制
// 临时存储响应式函数
const effectStack: ReactiveEffect[] = [] 

// 依赖收集栈
const trackStack: boolean[] = [] 

// 最大嵌套深度
const maxMarkerBits = 30
代码语言:javascript复制
export class ReactiveEffect<T = any> {
  run() {
     // 如果effect非激活状态,则返回原始副作用函数执行后的结果(fn就是被effect包裹的原始函数)
    if (!this.active) {
      return this.fn()
    }
    // 如effect不在effectStack栈中
    if (!effectStack.includes(this)) {
      try {
        // 当调用 effect 注册副作用函数时, 将副作用函数赋值给 activeEffect
        effectStack.push((activeEffect = this))
         // 在调用副作用函数之前将当前副作用函数压入栈中
        enableTracking()

        // 记录递归深度位数
        trackOpBit = 1 <<   effectTrackDepth

        // 如果 effect 嵌套层数没有超过 30 层
        if (effectTrackDepth <= maxMarkerBits) {
          // 给依赖打标记,for循环遍历 _effect 实例中的 deps 属性,
          // 给每个 deps 的 w 属性标记为 trackOpBit 的值
          // deps就是用来存储所有与该副作用函数相关联的依赖集合
          initDepMarkers(this)
        } else {
          // 清除当前 effect 相关依赖
          cleanupEffect(this)
        }
        return this.fn()
      } finally {
        // 完成依赖标记
        if (effectTrackDepth <= maxMarkerBits) {
          finalizeDepMarkers(this)
        }

        // 恢复到上一级
        trackOpBit = 1 << --effectTrackDepth

        // 重置依赖收集状态
        resetTracking()
        // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,
        // 并把 activeEffect 还原为之前的值(始终让activeEffect 指向栈顶的副作用函数)
        effectStack.pop()
        const n = effectStack.length
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }
}

run方法内在每次effrct执行时会将activeEffect设置为当前激活的副作用函数并且入栈,待副作用函数执行完毕后会将其从栈中弹出,并始终让activeEffect指向栈顶的副作用函数。这样就能做到一个响应式数据只会收集直接读取其值的副作用函数,而不会出现相互影响的情况。

track()

track 就是依赖收集器,负责把依赖收集起来统一放到一个依赖管理中心

具体则是以 key 为维度,将每一个 key 关联的副作用函数收集起来,存放在一个 Set 数据结构中,并以键值对的形式存储在 depsMap 的 Map 结构中

代码语言:javascript复制
/**
 * @description: 
 * @param {target} 目标对象 
 * @param {type} 收集的类型(GET = 'get', HAS = 'has', ITERATE = 'iterate') 
 * @param {key} 触发 track 的 object 的 key 
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 不启用依赖收集 则直接return
  if (!isTracking()) {
    return
  }

  // targetMap 依赖管理中心,用于收集依赖和触发依赖
  // 在 targetMap 中获取对应的 target 的依赖集合
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // 如果 target 不在 targetMap 中,则新建一个,并初始化 value 为 new Map()
    targetMap.set(target, (depsMap = new Map()))
  }
  // deps 来收集依赖函数,当监听的 key 值发生变化时,触发 dep 中的依赖函数
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined

  trackEffects(dep, eventInfo)
}

这里我们已经成功将依赖收集了起来,接下来就是看当我们修改响应式数据时,vue是如何知晓并更新的

三、trigger() 派发更新

trigger 是 track 收集的依赖对应的触发器,也就是负责根据映射关系,获取响应式函数,再派发通知 triggerEffects 进行更新

代码语言:javascript复制
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target)
  // 依赖管理中心没有, 代表没有收集过依赖,直接返回
  if (!depsMap) {
    // never been tracked
    return
  }

  let deps: (Dep | undefined)[] = []
  // 传入的类型为清除
  if (type === TriggerOpTypes.CLEAR) {
    // 收集被清除
    // 触发目标效果
    deps = [...depsMap.values()]
  } else if (key === 'length' &amp;&amp; isArray(target)) {
    // 如果类型为数组,并且数组的 length 改变时
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // 如果 key 不是 undefined,就添加对应依赖到队列,比如新增、修改、删除
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    // 根据type类型 处理新增、修改、删除逻辑
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          // 往队列中添加关联的所有依赖,准备清除
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  // 到这里就拿到了 targetMap[target][key],并存到 deps 里
  // 接着是要将对应的 effect 取出,调用 triggerEffects 执行

  // 开发环境赋值 eventInfo
  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined

  // deps内只有一个effect时直接取出执行,多个依赖则循环执行
  if (deps.length === 1) {
    if (deps[0]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
  } else {
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

这里根据映射关系已经将依赖函数全部收集至了deps中,接着就是遍历deps取出effect副作用函数并执行

triggerEffects()

这里就是派发更新的最后一步更新,从deps中取出effect副作用函数并执行,这样也就达到了响应式更新的作用。

代码语言:javascript复制
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // 遍历 effect 的集合函数
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ &amp;&amp; effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      // 有 scheduler 则执行
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        // 执行 effect 副作用函数
        effect.run()
      }
    }
  }
}

四、总结

看完本篇文章不知道你对依赖收集以及派发更新是否已经有了一个基本的认识,那这里呢我们就最后在总结一下依赖收集以及派发更新的全过程。


在读取我们的响应式数据时,响应式数据的get会拦截我们的读取操作,并通过track()进行依赖收集。在track()内会使用ReactiveEffect将我们的原始副作用函数注册为统一的effect副作用函数并存入targetMap(存储副作用函数的桶,WeakMap数据结构)中,targetMap的键是原始对象 target,值是一个Map实例,而Map的键是原始对象 target 的key,值是一个由副作用函数组成的Set

这样一个完整的映射关系就建立了。当我们监听的key发生变化时,就可以通过映射关系取出相对应的依赖函数就是effect,取出后就可以调用triggerEffects并执行effect.run(),这样就实现了派发更新


至此,本篇文章就结束了,如有问题欢迎各位大佬指导。若文内有任何不理解的地方也欢迎评论区提问~

0 人点赞