likes
comments
collection
share

Vue3源码阅读——响应式是如何实现的(track + trigger篇)

作者站长头像
站长
· 阅读数 9

前言

本文属于笔者Vue3源码阅读系列第五篇文章,往期精彩:

  1. 生成vnode到渲染vnode的过程是怎样的
  2. 组件创建及其初始化过程
  3. 响应式实现——reactive篇
  4. 响应式是如何实现的(ref + ReactiveEffect篇)

响应式部分源码已经产出两篇文章了,本文是最后一篇响应式相关的文章,主要内容为触发更新的过程分析,顺带看下track函数(之前漏了)。

track

/**
 * Tracks access to a reactive property.
 *
 * This will check which effect is running at the moment and record it as dep
 * which records all effects that depend on the reactive property.
 *
 * @param target - Object holding the reactive property.
 * @param type - Defines the type of access to the reactive property.
 * @param key - Identifier of the reactive property to track.
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) {
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep()))
    }

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

    trackEffects(dep, eventInfo)
  }
}

这个函数用于 track 对象中key 的依赖。有三个参数:

  1. target 就是数据对象。
  2. TrackOpTypes track操作类型,有以下三种。
    export const enum TrackOpTypes {
       GET = 'get', // target.key
       HAS = 'has', // key in target
       ITERATE = 'iterate' // 遍历
     }
    
  3. key 对应数据对象的属性名,如果是数组,那就是索引。

track中主要逻辑就是在确定dep参数,确定了以后再调用trackEffects,关于trackEffects在上一篇文章中已经阅读过了,文章都是连起来的,不建议跳读哦! 大致逻辑如下:

  1. 判断shouldTrack && activeEffect,只有为true才会track
  2. 先从targetMap中获取这个target对应的depsMaptargetMap是一个全局的WeakMap对象,用于存对象对应的dep。如果 depsMap 为空,则初始化一个new Map()进去。然后接着在depsMap 中根据 key 找到这个 key 对应的dep,如果没有,就初始化为 createDep()createDep也在上一篇文章中分析过,他会返回 dep = new Set(); dep.w = 0; dep.n = 0;确定了dep对象后就可以调用trackEffects了。看下targetMap的定义:
     // The main WeakMap that stores {target -> key -> dep} connections.
     // Conceptually, it's easier to think of a dependency as a Dep class
     // which maintains a Set of subscribers, but we simply store them as
     // raw Sets to reduce memory overhead.
     type KeyToDepMap = Map<any, Dep>
     const targetMap = new WeakMap<any, KeyToDepMap>()
    
    根据这个逻辑,我们能够得到targetMap的一个大致结构,如下图所示:

Vue3源码阅读——响应式是如何实现的(track + trigger篇)

targetMap还是一个WeakMap,顺便聊聊使用WeakMap的意义。

targetMap 使用 WeakMap 的意义

先说一下Map,我们都知道MapObject很类似,不同点在于Mapkey可以是任何类型。而Objectkey只能是字符串。

WeakMap的特点如下:

  1. key只能是Object,除了null之外,typeof obj === 'object' 的都可以当做key
  2. WeakMap没有遍历的方法。
  3. WeakMapkey是弱引用,即垃圾回收机制不将该引用考虑在内,举个例子:
     const e1 = document.getElementById('foo');
     const e2 = document.getElementById('bar');
     const arr = [
       [e1, 'foo 元素'],
       [e2, 'bar 元素'],
     ];
     // 不需要 e1 和 e2 的时候
     // 必须手动删除引用
     arr [0] = null;
     arr [1] = null;
     
    // 不需要处理引用
    const wm = new WeakMap()
    wm.set(e1, 1)
    wm.set(e2, 2)
    
  4. WeakMapvalue还是正常的引用。

targetMap 使用 WeakMap 的意义就在于对垃圾回收更有利。接着分析下触发更新的过程。

trigger

/**
 * Finds all deps associated with the target (or a specific property) and
 * triggers the effects stored within.
 *
 * @param target - The reactive object.
 * @param type - Defines the type of the operation that needs to trigger effects.
 * @param key - Can be used to target a specific reactive property in the target object.
 */
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 还是从targetMap获取depsMap,如果获取不到
  // 代表从来没有track过,直接return
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }
  // 用于存需要trigger的dep
  let deps: (Dep | undefined)[] = []
  // 如果是Map|Set 的clear操作
  // depsMap中的所有dep都需要去trigger
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  }
  // 如果key是length,并且target是Array
  // 只需要 trigger length对应的dep 和 索引大于等于 newlength对应的dep
  // const arr = reactive([1,2,3,4]),在template中使用到了arr
  // 当执行 arr.length = 3 就会走到这里,deps就会有两项
  // 一个是length对应的dep,一个是索引3对应的dep
  else if (key === 'length' && isArray(target)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= newLength) {
        deps.push(dep)
      }
    })
  } else {
    // 处理 SET | ADD | DELETE 这三种 TriggerOpTypes
    // schedule runs for SET | ADD | DELETE
    // key 不为 undefined,从 depsMap 获取一下这个 key 对应的 dep,并添加到deps
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      // ADD TriggerOpTypes
      case TriggerOpTypes.ADD:
        // target不是Array
        if (!isArray(target)) {
          // 添加depsMap.get(Symbol(__DEV__ ? 'iterate' : ''))
          // Map|Set 的size、forEach、keys(only Set)、values、entries
          // depsMap中depsMap.get(ITERATE_KEY),就有值
          deps.push(depsMap.get(ITERATE_KEY))
          // 如果是Map
          if (isMap(target)) {
            // 添加depsMap.get(Symbol(__DEV__ ? 'Map key iterate' : ''))
            // 只有当用到了Map 的 keys
            // depsMap中depsMap.get(MAP_KEY_ITERATE_KEY),才有值
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // 当key是正整数,数组中加了新的索引,那就直接trigger length的dep
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE
        // DELETE TriggerOpTypes
        // 此处的处理逻辑和 ADD TriggerOpTypes一致
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        // SET TriggerOpTypes
        if (isMap(target)) {
          // 如果是Map,添加depsMap.get(Symbol(__DEV__ ? 'iterate' : ''))
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined

  if (deps.length === 1) {
    if (deps[0]) {
      // 如果deps中只有一个dep,直接triggerEffects
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
  } else {
    // 有多个dep,先遍历去掉undefined的项
    // 并将所有dep中的effect都添加到effects
    // 然后调用createDep(effects),这将返回包含effect的dep实例
    // 然后调用triggerEffects
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}

当我们更改了target中的属性,可能是新增、删除、清空等,就会触发trigger,从trigger的逻辑不难看出来,它和track很像,大部分的逻辑都是在确定dep,与track不同的是,triggerdep可能有多个,确定了deps再调用triggerEffects。源码中我们已经添加了很多注释,接下来我们分析一下为啥要这么写:

  1. targetMap获取depsMap,如果获取不到,代表从来没有track过,直接return
  2. 如果是Map|Setclear操作,depsMap中的所有dep都需要去trigger,这个很好理解,对象被清空了,所以任何用到对象中任何一项的dep都需要被trigger
  3. 如果keylength,并且targetArray,只需要 trigger length对应的dep 和 索引大于等于 newlength对应的dep。这个也很好理解,索引大于等于newlength的被删了,length变了。
const arr = reactive([1,2,3,4])

function onClick() {
  arr.length = 3
}
// 在template中使用到了arr
// 当执行 arr.length = 3,deps 就会有两项
// 一个是length对应的dep,一个是索引3对应的dep
  1. 最后一个分支是处理ADD|SET|DELETE。先 deps.push(depsMap.get(key)),对于ADD操作来说,这个肯定是undefined,不过SET|DELETE则可能有对应的dep,然后针对ADD|SET|DELETE分别处理。

  2. 处理ADD操作时,区分了数组非数组:

    数组:当key是正整数,数组中索引增加了,那就直接trigger lengthdep

    ```js
     const arr = reactive([1,2,3,4])
     // 以下操作都会触发此处的ADD逻辑
     function onClick() {
       arr.push(6)
       // arr.splice(arr.length, 0, 6)
       // arr[4] = 6
       // ......
     }
    ```
    

    非数组:先将 keyITERATE_KEYdep 放进去(不管有没有),因为最后会把 undefined 的过滤掉;如果targetMap,还要将 keyMAP_KEY_ITERATE_KEYdep 放进去。咱们来详细分析下为啥要这样处理。首先来看下什么情况能够 trackITERATE_KEYMAP_KEY_ITERATE_KEY,看下图:

    Vue3源码阅读——响应式是如何实现的(track + trigger篇)

    如上图所示,如果在副作用函数中用到以上图中对象的一些属性或者方法,就会 trackkeyITERATE_KEYMAP_KEY_ITERATE_KEYdep。当 trigger ADD 操作时,可能是Map|WeakMap|Set|WeakSetADD,也可能是 Object 的新增属性,但是不论是任何一种,都会影响对象的遍历结果,因此需要trigger keyITERATE_KEYMAP_KEY_ITERATE_KEYdep。不知道你们会不会想到一个问题——为啥map的keys()那么特殊,要单独用一个key?

  3. 处理DELETE操作时,可能是Map|WeakMap|Set|WeakSetdelete,也可能是删除Object的属性,但是不论是任何一种,都会影响对象的遍历结果,因此需要trigger keyITERATE_KEYMAP_KEY_ITERATE_KEYdep。与处理ADD操作的非数组逻辑一致。

  4. 处理SET操作时,有两种情况会走到这里,一种是 obj.xxx = 'newValue',这种情况在一开始已经处理了——deps.push(depsMap.get(key)); 另一种是 MapsetSet 没有 set 操作),Mapset 操作影响的是 map对象 除了 keys() 之外的遍历操作(value、entries、forEach、Symbol.iterator),因此只需要trigger keyITERATE_KEYdep我相信看到这里解释了上面的问题,因为 mapset 不会改变 keys() 的结果,如果map.keys()track keyITERATE_KEYdep,此时调用 map.set 会造成不必要的 triggerVue的源码永远细节满满👍🏻

我相信如果你看到这里,trigger 的逻辑肯定看明白了,接下来咱们就看 triggerEffects

triggerEffects

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  // 没看懂为什么这里要判断dep是不是数组,因为就不可能是数组,而且不是数组为啥要把dep里的effects放到数组,Set也可以for of的,可能是历史原因,还没改过来吧!
  const effects = isArray(dep) ? dep : [...dep]
  // 先 trigger computed的effect,先执行computed的副作用函数
  // 因为其他的副作用函数可能依赖computed的value
  for (const effect of effects) {
    if (effect.computed的effect) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  // 再trigger别的effect
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

针对这一行多余的代码const effects = isArray(dep) ? dep : [...dep],我给vuejs提了issue

Vue3源码阅读——响应式是如何实现的(track + trigger篇)

如果被成功合并到主分支,那咱们的issue还是有意义的:

Vue3源码阅读——响应式是如何实现的(track + trigger篇)

接下来看下triggerEffect的逻辑:

triggerEffect

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // 如果 effect 不等于 全局activeEffect 或者 effect.allowRecurse
  if (effect !== activeEffect || effect.allowRecurse) {
    // 开发模式,有effect.onTrigger的话,执行effect.onTrigger
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    // 调度执行器副作用函数
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      // 直接调用run
      effect.run()
    }
  }
}

triggerEffect就是负责调用 effect.run,不过调用的方式有所不同,如果有调度器,就会通过调度器来调用,没有就直接调用。当我触发一个变更,导致了组件的渲染函数重新执行,组件的渲染函数就是通过调度器执行的:

// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
  componentUpdateFn,
  // 调度器
  () => queueJob(update),
  instance.scope // track it in component's effect scope
))

const update: SchedulerJob = (instance.update = () => effect.run())
update.id = instance.uid

下图是调用堆栈:

Vue3源码阅读——响应式是如何实现的(track + trigger篇)

从调用堆栈我们能够看到effect.run会异步执行。这块咱们后续详细阅读吧。

Promise.then(effect.run)

最后,咱们通过一个简单的例子,画图来串一下tracktrigger的完整过程。

结合例子串联响应式逻辑

结合一个例子来看可能会更好理解:

// Main.vue
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const visible = ref(true)
const count = ref(0)

function onClick() {
  visible.value = !visible.value
}

</script>

<template>
  <div>
    <div v-if="visible">{{ count }}</div>
    <button @click="onClick">click</button>
    <Child :visible="visible"></Child>
  </div>
</template>

// Child.vue
<template>
  <div>{{ props.visible }}</div>
</template>

<script setup>
const props = defineProps(['visible'])
</script>

在上面的例子中,Main.vue的渲染 effect.run,对象初始化会 track visible count 对应的 depChild.vue 的渲染 effect 对象会 track visible 对应的dep,如下图所示:

Vue3源码阅读——响应式是如何实现的(track + trigger篇)

当点击一次 click 按钮,visible.value = false; 此时将触发Main.vue的渲染effect.run,由于 visiblefalse,将不会 track count 对应的 depChild.vue 的渲染 effect 对象的 deps 则不会有什么影响,如下图所示:

Vue3源码阅读——响应式是如何实现的(track + trigger篇)

以上图示就是 tracktrigger 的完整过程。

总结

回顾一下本文主要内容:

  1. track
  2. trigger
  3. triggerEffects
  4. triggerEffect
  5. tracktrigger 的完整过程分析。

这是笔者第五篇源码分析类的文章,如果掘友们有什么建议,或者文中有错误,还请评论指出,谢谢!

如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^