Vue3源码阅读——响应式是如何实现的(track + trigger篇)
前言
本文属于笔者Vue3源码阅读系列第五篇文章,往期精彩:
响应式部分源码已经产出两篇文章了,本文是最后一篇响应式相关的文章,主要内容为触发更新的过程分析,顺带看下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 的依赖。有三个参数:
target就是数据对象。TrackOpTypestrack操作类型,有以下三种。export const enum TrackOpTypes { GET = 'get', // target.key HAS = 'has', // key in target ITERATE = 'iterate' // 遍历 }key对应数据对象的属性名,如果是数组,那就是索引。
track中主要逻辑就是在确定dep参数,确定了以后再调用trackEffects,关于trackEffects在上一篇文章中已经阅读过了,文章都是连起来的,不建议跳读哦!
大致逻辑如下:
- 判断
shouldTrack && activeEffect,只有为true才会track; - 先从
targetMap中获取这个target对应的depsMap,targetMap是一个全局的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的一个大致结构,如下图所示:

targetMap还是一个WeakMap,顺便聊聊使用WeakMap的意义。
targetMap 使用 WeakMap 的意义
先说一下Map,我们都知道Map和Object很类似,不同点在于Map的key可以是任何类型。而Object的key只能是字符串。
WeakMap的特点如下:
key只能是Object,除了null之外,typeof obj === 'object'的都可以当做key。WeakMap没有遍历的方法。WeakMap对key是弱引用,即垃圾回收机制不将该引用考虑在内,举个例子: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)WeakMap对value还是正常的引用。
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不同的是,trigger时dep可能有多个,确定了deps再调用triggerEffects。源码中我们已经添加了很多注释,接下来我们分析一下为啥要这么写:
- 从
targetMap获取depsMap,如果获取不到,代表从来没有track过,直接return。 - 如果是
Map|Set的clear操作,depsMap中的所有dep都需要去trigger,这个很好理解,对象被清空了,所以任何用到对象中任何一项的dep都需要被trigger。 - 如果
key是length,并且target是Array,只需要triggerlength对应的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
-
最后一个分支是处理
ADD|SET|DELETE。先deps.push(depsMap.get(key)),对于ADD操作来说,这个肯定是undefined,不过SET|DELETE则可能有对应的dep,然后针对ADD|SET|DELETE分别处理。 -
处理
ADD操作时,区分了数组、非数组:数组:当
key是正整数,数组中索引增加了,那就直接triggerlength的dep```js const arr = reactive([1,2,3,4]) // 以下操作都会触发此处的ADD逻辑 function onClick() { arr.push(6) // arr.splice(arr.length, 0, 6) // arr[4] = 6 // ...... } ```非数组:先将
key为ITERATE_KEY的dep放进去(不管有没有),因为最后会把undefined的过滤掉;如果target是Map,还要将key为MAP_KEY_ITERATE_KEY的dep放进去。咱们来详细分析下为啥要这样处理。首先来看下什么情况能够track到ITERATE_KEY和MAP_KEY_ITERATE_KEY,看下图:
如上图所示,如果在副作用函数中用到以上图中对象的一些属性或者方法,就会
track到key为ITERATE_KEY或MAP_KEY_ITERATE_KEY的dep。当triggerADD操作时,可能是Map|WeakMap|Set|WeakSet的ADD,也可能是Object的新增属性,但是不论是任何一种,都会影响对象的遍历结果,因此需要triggerkey为ITERATE_KEY或MAP_KEY_ITERATE_KEY的dep。不知道你们会不会想到一个问题——为啥map的keys()那么特殊,要单独用一个key? -
处理
DELETE操作时,可能是Map|WeakMap|Set|WeakSet的delete,也可能是删除Object的属性,但是不论是任何一种,都会影响对象的遍历结果,因此需要triggerkey为ITERATE_KEY或MAP_KEY_ITERATE_KEY的dep。与处理ADD操作的非数组逻辑一致。 -
处理
SET操作时,有两种情况会走到这里,一种是obj.xxx = 'newValue',这种情况在一开始已经处理了——deps.push(depsMap.get(key)); 另一种是Map的set(Set没有set操作),Map的set操作影响的是map对象除了keys()之外的遍历操作(value、entries、forEach、Symbol.iterator),因此只需要triggerkey为ITERATE_KEY的dep。我相信看到这里解释了上面的问题,因为map的set不会改变keys()的结果,如果map.keys()也trackkey为ITERATE_KEY的dep,此时调用map.set会造成不必要的trigger。Vue的源码永远细节满满👍🏻
我相信如果你看到这里,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:

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

接下来看下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
下图是调用堆栈:

从调用堆栈我们能够看到effect.run会异步执行。这块咱们后续详细阅读吧。
Promise.then(effect.run)
最后,咱们通过一个简单的例子,画图来串一下track和trigger的完整过程。
结合例子串联响应式逻辑
结合一个例子来看可能会更好理解:
// 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 对应的 dep;Child.vue 的渲染 effect 对象会 track visible 对应的dep,如下图所示:

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

以上图示就是 track 和 trigger 的完整过程。
总结
回顾一下本文主要内容:
track。trigger。triggerEffects。triggerEffect。track和trigger的完整过程分析。
这是笔者第五篇源码分析类的文章,如果掘友们有什么建议,或者文中有错误,还请评论指出,谢谢!
如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【赞】都是我创作的最大动力 ^_^。
转载自:https://juejin.cn/post/7252283213187432505