关于javascript:Vue3-源码解析七依赖收集与副作用函数

3次阅读

共计 9024 个字符,预计需要花费 23 分钟才能阅读完成。

在上一篇文章《响应式原理与 reactive》中因为篇幅限度笔者留下了两个小悬念 track 依赖收集处理器与 trigger 派发更新处理器没有粗疏解说,而在本篇文章中笔者会带着大家一起来学习 Vue3 响应式零碎中的依赖收集局部和副作用函数。

Vue 是怎么追踪变动的?

当咱们在 template 模板中应用响应式变量,或者在计算属性中传入 getter 函数后当计算属性中的源数据发生变化后,Vue 总能即时的告诉更新并从新渲染组件,这些神奇的景象是如何实现的呢?

Vue 通过一个副作用(effect)函数来跟踪以后正在运行的函数。副作用是一个函数包裹器,在函数被调用前就启动跟踪,而 Vue 在派发更新时就能精确的找到这些被收集起来的副作用函数,当数据产生更新时再次执行它。

为了更好的了解依赖的收集过程,笔者先从副作用函数的实现开始说起。

effect 的类型

老规矩在介绍副作用之前,先一起看一下副作用的类型,这样可能帮忙大家先对副作用“长的什么样子”有一个直观的概念。

export interface ReactiveEffect<T = any> {(): T
  _isEffect: true
  id: number
  active: boolean
  raw: () => T
  deps: Array<Dep>
  options: ReactiveEffectOptions
  allowRecurse: boolean
}

从副作用的类型定义中能够清晰的看到它定义了一个泛型参数,这个泛型会被当做外部副作用函数的返回值,并且这个类型自身就是一个函数。还有一个 _isEffect 属性标识这是一个副作用;active 属性是用来标识这个副作用启用和停用的状态;raw 属性保留初始传入的函数;deps 属性是这个副作用的所有依赖,对于这个数组中元素的 Dep 类型咱们笔者就会介绍到;options 中保留着副作用对象的一些配置项;而 allowRecurse 临时不必关注,它是一个副作用函数是否本身调用的标识。

副作用的全局变量

有三个变量是定义在副作用模块中的全局变量,而提前意识这些变量可能帮忙咱们理解整个副作用函数的生成以及调用的过程。

type Dep = Set<ReactiveEffect>
type KeyToDepMap = Map<any, Dep>
const targetMap = new WeakMap<any, KeyToDepMap>()

const effectStack: ReactiveEffect[] = []
let activeEffect: ReactiveEffect | undefined

targetMap:

这个 targetMap 是一个十分重要的变量,它是 WeakMap 类型,存储了 {target -> key -> dep} 的链接。

targetMap 的值的类型是 KeyToDepMap,而 KeyToDepMap 又是一个以 Dep 为值的类型的 Map 对象,Dep 就是笔者始终在提及的依赖,Vue 收集依赖其实就是在收集 Dep 类型。所以对照 Vue2 的源码,从概念上来讲,将依赖看成是一个保护了订阅者 Set 汇合的 Dep 类更容易了解,在 targetMap 中只是将 Dep 存储在一个原始的 Set 汇合中,是出于缩小内存开销的思考。

effectStatck

这是一个寄存以后正被调用的副作用的栈,当一个副作用在执行前会被压入栈中,而在完结之后会被推出栈。

activeEffect

这个变量标记了以后正在执行的副作用,或者也能够了解为副作用栈中的栈顶元素。当一个副作用被压入栈时,会将这个副作用赋值给 activeEffect 变量,而当副作用中的函数执行完后该副作用会出栈,并将 activeEffect 赋值为栈的下一个元素。所以当栈中只有一个元素时,执行完出栈后,activeEffect 就会为 undefined。

副作用(effect)的实现

在学习完须要前置了解的类型与变量后,笔者就开始解说副作用函数的实现,话不多说间接看代码。

export function effect<T = any>(fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  // 如果 fn 曾经是一个副作用函数,则返回副作用的原始函数
  if (isEffect(fn)) {fn = fn.raw}
  // 创立一个副作用
  const effect = createReactiveEffect(fn, options)
  // 如果不是提早执行的,则立刻执行一次副作用函数
  if (!options.lazy) {effect()
  }
  // 返回生成的副作用函数
  return effect
}

effect api 的函数绝对简略,当传入的 fn 曾经是一个副作用函数时,会将 fn 赋值为这个副作用的原始函数。接着会调用 createReactiveEffect 创立一个 ReactiveEffect 类型的函数,如果副作用的选项中没有设置提早执行,那么这个副作用函数会被立刻执行一次,最初将生成的副作用函数返回。

接着一起来看创立副作用函数的 createReactiveEffect 的逻辑。

createReactiveEffect

在 createReactiveEffect 中,首先会创立一个变量名为 effect 的函数表达式,之后为这个函数设置之前在 ReactiveEffect 类型中提及到的一些属性,最初将这个函数返回。

而当这个 effect 函数被执行时,会首先判断本人是不是曾经停用,如果是停用状态,则会查看选项中是否有调度函数,如果有调度函数就不再解决,间接 return undefined,若是不存在调度函数,则执行并返回传入的 fn 函数,之后就不再运行上来。

如果 effect 函数状态失常,会判断以后 effect 函数是否曾经在副作用栈中,若是曾经被退出栈中,则不再持续解决,防止循环调用。

如果以后 effect 函数不在栈中,就会通过 cleanup 函数清理副作用函数的依赖,并且关上依赖收集开关,将副作用函数压入副作用栈中,并记录以后副作用函数为 activeEffect。这段逻辑笔者在介绍这两个变量时曾经讲过,它就是在此处触发的。

接下来就会执行传入的 fn 函数被返回后果。

当函数执行结束后,会将副作用函数弹出栈中,并且将依赖收集开关重置为执行副作用前的状态,再将 activeEffect 标记为以后栈顶的元素。此时一次副作用函数的执行彻底完结,跟着笔者一起来看一下源码的实现。

function createReactiveEffect<T = any>(fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  // 通过一个函数表达式,创立一个变量名为 effect,函数名为 reactiveEffect 的函数
  const effect = function reactiveEffect(): unknown {
    // 如果 effect 已停用,当选项中有调度函数时返回 undefined,否则返回原始函数
    if (!effect.active) {return options.scheduler ? undefined : fn()
    }
    if (!effectStack.includes(effect)) {
      // 清理依赖
      cleanup(effect)
      try {
        // 容许收集依赖
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()} finally {effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  // 为副作用函数设置属性
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

当在最初一行 return 了副作用函数后,上一段提及提及当 options 参数中 lazy 为 false 时,这个副作用函数就会第一次被调用,此时就会触发这段函数 第 6 行 const effect 创立函数后的函数外部逻辑。

了解了 createReactiveEffect 的执行程序后,再配合具体的逻辑解说,置信你也曾经把握 effect 副作用函数的创立了。

收集依赖、派发更新

为了更逻辑顺畅的引出依赖收集和派发更新的工作及实现流程,笔者决定在此处引入一个 Vue3 中 effect 模块的一个简略的单元测试用例,给大家解说示例的同时顺带聊聊依赖收集和派发更新。

let foo
const counter = reactive({num: 0})
effect(() => (foo = counter.num))
// 此时 foo 应该是 0
counter.num = 7
// 此时 foo 应该是 7

这是一个最简略的 effect 的示例,咱们都晓得 foo 会随着 counter.num 的扭转而扭转。那么到底是如何更新的呢?

首先,counter 通过 reactive api 生成一个 proxy 代理对象。这一个生成过程在上一篇文章中曾经解说过了,所以这里就不细讲了。

接着应用 effect,向它传入一个函数。这时 effect 开始它的创立过程,在 effect 函数中会执行到下方代码的这一步。

const effect = createReactiveEffect(fn, options)

通过 createReactiveEffect 开始创立 effect 函数,并返回。

当 effect 函数被返回后,就会判断以后副作用的选项中是否须要提早执行,而这里咱们没有传入任何参数,所以不是提早加载,须要立刻执行,所以会开始执行返回回来的 effect 函数。

if (!options.lazy) {effect() // 不须要提早执行,执行 effect 函数
}

于是会开始执行 createReactiveEffect 创立 effect 函数时的外部代码逻辑。

const effect = function reactiveEffect(): unknown {/* 执行此函数内的逻辑 */}

因为 effect 函数是 active 状态,并且也不在副作用栈中,于是会先革除依赖,因为当初并没有收集任何依赖,所以 cleanup 的过程不必关怀。接着会将 effet 压入栈中,并设置为 activeEffect,接下来会开始执行初始传入的 fn:() => (foo = counter.num)

给 foo 赋值时,会先拜访 counter 的 num 属性,所以会触发 counter 的 proxy handler 的 get 陷阱:

// get 陷阱
return function get(target: Target, key: string | symbol, receiver: object) {
    /* 疏忽逻辑 */
  // 获取 Reflect 执行的 get 默认后果
  const res = Reflect.get(target, key, receiver)
  if (!isReadonly) {
    // 依赖收集
    track(target, TrackOpTypes.GET, key)
  }
  return res
}

这里我简化了 get 中的代码,只保留要害局部,能够看到在获取到 res 的值后,会通过 track 开始依赖收集。(🥺 留神要开始讲依赖收集了哦,不要走神)

track 收集依赖

track 函数的门路也是在 @vue/reactivity 库的 effect.ts 的文件中。

在 track 的过程中,首先会判断是否容许收集依赖,这个状态是受 enableTracking()pauseTracking() 这一对函数管制的。接着会判断以后是否有正在执行的副作用函数,如果没有则间接 return。因为依赖收集其实就是在收集 副作用函数

接着从本文一开始介绍过的 targetMap 中去尝试获取对应的 traget 的依赖汇合,并存储在 depsMap 变量中,如果获取失败,就会将以后 target 增加进依赖汇合中,并将 value 初始化为 new Map()。例如在以后的示例中,target 即为 { num: 0},是 counter 对象的值。

在有了 depsMap 后,就会依据 target 中被读取的 key,去依赖汇合中查看是否有对应 key 的依赖,并赋值给 dep。如果没有,就跟创立 depsMap 的逻辑一样,创立一个 Set 类型的汇合当做值。

如果以后执行的副作用函数没有被 dep 这个 Set 汇合当做依赖收集,就会将以后副作用函数增加进 dep 中,并且在以后的副作用函数的 deps 属性中增加进该依赖 dep。

看到这里,就可能设想出依赖的收集是一个什么样的构造了。以 key 为维度,将每一个 key 关联的副作用函数收集起来,寄存在一个 Set 数据结构中,并以键值对的模式存储在 depsMap 的 Map 构造中。此时再看文章结尾形容 targetMap 这个 Map 存储的模式 {target -> key -> dep} 应该说是十分明确了。

track 处理器函数的代码如下:

export function track(target: object, type: TrackOpTypes, key: unknown) {
  // 不启用依赖收集,或者没有 activeEffect 则间接 return
  if (!shouldTrack || activeEffect === undefined) {return}
  // 在 targetMap 中获取对应的 target 的依赖汇合
  let depsMap = targetMap.get(target)
  if (!depsMap) {// 如果 target 不在 targetMap 中,则退出,并初始化 value 为 new Map()
    targetMap.set(target, (depsMap = new Map()))
  }
  // 从依赖汇合中获取对应的 key 的依赖
  let dep = depsMap.get(key)
  if (!dep) {// 如果 key 不存在,将这个 key 作为依赖收集起来,并初始化 value 为 new Set()
    depsMap.set(key, (dep = new Set()))
  }
  // 如果依赖中并不存以后的 effect 副作用函数
  if (!dep.has(activeEffect)) {
    // 将以后的副作用函数收集进依赖中
    dep.add(activeEffect)
    // 并在以后副作用函数的 deps 属性中记录该依赖
    activeEffect.deps.push(dep)
  }
}

看完 track 持续看咱们的示例:

effect(() => (foo = counter.num))

当 track 收集完依赖后,get 陷阱返回了 Reflect.get 的后果,读取到了 counter.num 的值为 0,并将此后果赋值给 foo 变量。此时副作用 函数第一次运行完结,foo 曾经有了值:0。当副作用函数执行完,会将以后的副作用函数弹出栈中,并且将 activeEffect 赋值为 undefeind。

trigger 派发更新

搞懂了依赖收集之后,持续来看派发更新的过程。

示例的最初一行代码,将 num 赋值为 7。

counter.num = 7

咱们晓得 foo 肯定会同步更新为 7 的。那么过程是怎么的呢?

当对 counter.num 赋值时,会触发 set 陷阱:

const result = Reflect.set(target, key, value, receiver)
if (target === toRaw(receiver)) {if (!hadKey) {
    // 当 key 不存在时,触发 trigger 的 ADD 事件
    trigger(target, TriggerOpTypes.ADD, key, value)
  } else if (hasChanged(value, oldValue)) {
    // 当 key 存在时,当新旧值变动后,触发 trigger 的 SET 事件
    trigger(target, TriggerOpTypes.SET, key, value, oldValue)
  }
}
return result

一起来看 set 陷阱的局部代码,trigger 的触发会传入一个 TriggerOpTypes 的枚举,枚举有四种类型,对应增、删、改、清空操作。

export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}

因为 counter 通过 reactive api 创立代理对象时曾经增加了 num 这个 key,所以此时新旧值产生扭转,就会触发 SET 事件。

接着会执行 trigger 函数。

trigger 函数会立刻从 targetMap 中通过 target 获取 depsMap,如果没有对应的 depsMap 就代表以后的 traget 从未通过 track 进行依赖收集,所以间接 return,不继续执行。

接着会创立一个名为 effects 的 Set 构造的汇合,它的作用是存储这个 key 所有须要派发更新执行的副作用函数。

同时申明一个 add 函数,add 函数的作用是遍历传入的副作用函数,将不是以后正在执行的 activeEffect 函数或者可能自我执行的副作用函数都退出到 effects 汇合中。

而后会判断清空依赖和数组的非凡状况,按需调用 add 函数增加依赖。

之后会判断以后 key 是否不为 undefined,留神这里的判断条件 void 0,是通过 void 运算符的模式示意 undefined,如果有 key 则将 key 相干的依赖通过 add 函数增加进 effects 汇合中。

随后的 Switch Case 通过辨别 triggerOpTypes 来解决一些迭代键的非凡逻辑。

之后申明的 run 函数作用就是来执行增加入 effects 数组中的副作用函数。

trigger 函数的结尾就是通过 effects.forEach(run) 遍历汇合内的所有副作用函数并执行。

先一起来看一下 trigger 的代码:

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) {
        // 该 target 从未被追踪,不继续执行
    return
  }
    
  // effects 汇合寄存所有须要派发更新的副作用函数。const effects = new Set<ReactiveEffect>()
  // 将不是以后副作用函数以及能执行本身的副作用函数退出汇合中
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {if (effectsToAdd) {
      effectsToAdd.forEach(effect => {if (effect !== activeEffect || effect.allowRecurse) {effects.add(effect)
        }
      })
    }
  }

  if (type === TriggerOpTypes.CLEAR) {
        // 当须要革除依赖时,将以后 target 的依赖全副传入
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    // 解决数组的非凡状况
    depsMap.forEach((dep, key) => {if (key === 'length' || key >= (newValue as number)) {add(dep)
      }
    })
  } else {
    // 在 SET | ADD | DELETE 的状况,增加以后 key 的依赖
    if (key !== void 0) {add(depsMap.get(key))
    }

    // 对 ADD | DELETE | Map.SET 执行一些迭代键的逻辑
    switch (type) {/* 临时疏忽 */}
  }
    
  // 执行 effect 的函数
  const run = (effect: ReactiveEffect) => {
    // 判断是否有调度器,如果有则执行调度函数并将 effect 作为参数传入
    if (effect.options.scheduler) {effect.options.scheduler(effect)
    } else {
      // 否则间接执行副作用函数
      effect()}
  }
    // 遍历汇合,执行收集到的副作用函数
  effects.forEach(run)
}

在将 SwitchCase 的非凡逻辑,以及 DEV 环境的非凡逻辑暗藏后,trigger 函数的长度曾经比拟精简且逻辑清晰了。

回到咱们的示例,当在 trigger 判断是否有 key,并将 key 对应的依赖传入 add 函数时,示例在 track 时被收集的副作用函数曾经被 effects 汇合获取到了。当 trigger 执行到最初一行代码时,副作用函数就会当做参数被传入 run 函数,因为没有设置调度器,所以会间接执行这个副作用函数:() => (foo = counter.num),执行结束,foo 的值胜利的被更新到 7。

至此收集依赖和派发更新的流程曾经残缺的完结,而本文的示例也运行结束了,置信大家对这个过程也有了印象粗浅的意识。如果还是有点犯迷糊,倡议将本文 effect, track 和 trigger 的函数,以及上文 get、set 的陷阱源码分割起来再看看,置信你会恍然大悟的。

总结

本篇文章中,笔者先给大家具体解说了副作用曾经副作用函数的生成过程以及执行机会。又通过一个简略的示例引出依赖收集和派发更新的过程,在将这两个局部时,联合上文中讲过的 get 和 set 这两个代理对象的 hanlders 陷阱将流程残缺的串在一起,依照示例执行的流程给大家讲完了整个依赖收集和派发更新的过程。

这也解答了文章结尾提到的问题:Vue 是如何追踪变动的?通过 track 收集副作用的依赖,并在 trigger 时执行对应的副作用函数实现更新。

最初,如果这篇文章可能帮忙到你理解 Vue3 中的响应式的副作用以及依赖收集和派发更新的流程,心愿能给本文点一个喜爱❤️。如果想持续追踪后续文章,也能够关注我的账号或 follow 我的 github,再次谢谢各位可恶的看官老爷。

正文完
 0