前几章完整的介绍了 Vue3 的响应式核心reactive
和effect
的实现原理,这一章我们来看看Vue3
的依赖收集和依赖触发是如何工作的。
根据之前的分析,我们知道依赖收集是在reactive
中的get
钩子中完成的(不是所有),而依赖触发是在set
钩子中完成的(也不是所有),依赖指的是effect
。
这里的特点是get
是在取值,set
是在赋值,那么Vue3
是如何正确的收集依赖和触发依赖的呢?接下来我们来一起看看。
依赖收集
在讲解之前的章节的时候,我们知道reactive
中的get
钩子中会调用track
方法,我特意的避开了这一块的内容,讲的很浅,因为讲解这个方法的时候,我们需要先了解Vue3
中的effect
是如何工作的;
而现在已经讲过了effect
的实现原理,我们来看看track
方法是如何工作的,会很方便的理解Vue3
中的依赖收集,我们还是来回一下get
钩子的代码:
function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { // 判断是否是数组 const targetIsArray = isArray(target); // 对数组原型上的方法进行特别对待 if (targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver); } // 获取结果 const res = Reflect.get(target, key, receiver); // 收集依赖 track(target, "get" /* TrackOpTypes.GET */, key); // 返回结果 return res; }; }
上述代码解释来自:【源码&库】 Vue3 的依赖收集,这里的依赖指代的是什么?
如果想看详细的具体源码解析可以看这一章:【源码&库】Vue3 的响应式核心 reactive 和 effect 实现原理以及源码分析
可以看到get
钩子中会在获取到值之后,调用track
方法,其中会传入三个参数,我们来看看track
方法的实现:
// 依赖映射表,用来存储对象的依赖 const targetMap = new WeakMap(); /** * * @param target 目标对象,指向的是当前操作的对象 * @param type 操作类型,有 get/has/iterate 三种,会面会讲到区别 * @param key 操作的 key,指向的是当前操作对象的属性名 */ function track(target, type, key) { // shouldTrack 和 activeEffect 在之前的章节中讲到过 // shouldTrack 用来判断当前是否需要收集依赖 // activeEffect 指向的是当前正在执行的 effect,收集依赖收集的就是它 // 如果 shouldTrack 为 false 或者 activeEffect 为 null,说明不需要收集依赖 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 = { effect: activeEffect, target, type, key } // 收集依赖 trackEffects(dep, eventInfo); } }
这里的依赖收集的逻辑看着比较简单,但是实际会涉及到很多的细节,我们来一一分析:
首先是shouldTrack
和activeEffect
,这两个变量在effect
中都出现过,但是activeEffect
是在effect
中赋值的,shouldTrack
在effect
也有赋值操作,但是值并不是由effect
直接控制的。
activeEffect
在之前的文章中反复的提到过了,这里就不再赘述,我们来看看shouldTrack
是如何工作的;
shouldTrack
这个变量是用来判断当前是否需要收集依赖的,全局搜索一下它的赋值操作,可以找到如下代码:
// 当前执行的 effect 是否需要收集依赖 let shouldTrack = true; // 调用链栈,用来处理嵌套的 effect 调用情况 // 记录这个链上的 effect 是否需要收集依赖 const trackStack = []; // 暂停依赖收集 function pauseTracking() { // 将当前的 shouldTrack 值压入栈中 trackStack.push(shouldTrack); // 将 shouldTrack 设置为 false,表示暂停收集依赖 shouldTrack = false; } // 恢复依赖收集,没有找打具体的用法,暂时不关心 function enableTracking() { trackStack.push(shouldTrack); shouldTrack = true; } // 重置依赖收集 function resetTracking() { // 弹出栈顶的 shouldTrack 值,表示当前的 shouldTrack 值已经失效,需要恢复上一个 shouldTrack 值 const last = trackStack.pop(); // 如果 last 为 undefined,说明栈中没有 shouldTrack 值,这时候将 shouldTrack 设置为 true shouldTrack = last === undefined ? true : last; }
这一块具体的使用场景在上一章已经讲过了,是在数组的push
、pop
、shift
、unshift
、splice
拦截器中使用的,可以去看:代理 Object | get 钩子
这里我们只需要知道,shouldTrack
是用来判断当前是否需要收集依赖的,如果shouldTrack
为false
,则不会收集依赖,如果shouldTrack
为true
,则会收集依赖。
依赖映射表 targetMap
首先我们来看看targetMap
,这个变量是用来存储对象的依赖的,它是一个WeakMap
,WeakMap
是弱引用的,具体的数据结构如下:
{ [target]: { [key]: [new Set([effect1, effect2])] } }
这里的target
指向的是当前操作的对象,key
指向的是当前操作对象的属性名,key
对应的值是一个Set
,Set
中存储的是当前对象属性的所有依赖,也就是effect
。
这里说的可能不是很好理解,我们来模拟一下:
// 依赖映射表 const targetMap = new WeakMap(); const track = (target, type, key) => { const depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } // 这里的 key 就是 name,也就是属性名 let dep = depsMap.get(key); if (!dep) { // 这里的 dep 就是 Set,也就是依赖集合 depsMap.set(key, (dep = new Set())); } // 添加到依赖集合中,这里的 activeEffect 就是当前正在执行的 effect dep.add(activeEffect); } // 创建一个对象,用来模拟 target const target = { name: '田八', age: 18 }; // 这里有两个 effect,用来模拟一个属性用在的两个地方 function effect1(fn) { console.log(target.name); } function effect2(fn) { console.log(target.name); console.log(target.age); } let activeEffect = null; // 只有 effect 在执行的时候才会收集依赖 // 实际情况是执行的过程中就会收集依赖,我们这里手动模拟 effect1(); activeEffect = effect1; // 收集依赖 track(target, 'get', 'name'); activeEffect = null; // 收集第二个依赖,流程同上 effect2(); activeEffect = effect2; // 收集依赖 track(target, 'get', 'name'); // 这个会多一个 age 的依赖 track(target, 'get', 'age'); activeEffect = null;
经过上述的流程,最后targetMap
的数据结构如下:
{ [target]: { name: [effect1, effect2] age: [effect2] } }
依赖收集的逻辑就是这样,只是做收集,真正的执行逻辑是在trigger
方法中,这个后面会讲到。
trackEffects
上面已经了解到了shouldTrack
和targetMap
,这里我们来看看trackEffects
,这个方法是用来收集依赖的,它的具体实现如下:
// maxMarkerBits 用来记录最大的依赖收集的深度 const maxMarkerBits = 30; // effectTrackDepth 用来记录当前的依赖收集的深度 let effectTrackDepth = 0; // trackOpBit 用来记录当前的依赖收集的深度,这个是配合 maxMarkerBits 使用的 let trackOpBit = 1 function trackEffects(dep, debuggerEventExtraInfo) { // shouldTrack 含义同上,但是不是全局的,而是局部的 let shouldTrack = false; // 这一步是为了解决递归调用的问题 if (effectTrackDepth <= maxMarkerBits) { // newTracked 用来判断当前的依赖是否新的依赖追踪 if (!newTracked(dep)) { // 设置新的依赖追踪 dep.n |= trackOpBit; // set newly tracked // wasTracked 用来判断当前的依赖是否已经被追踪过了 shouldTrack = !wasTracked(dep); } } else { // Full cleanup mode. // 已经到了最大的依赖收集深度,这里就不再对相同的依赖进行收集了 shouldTrack = !dep.has(activeEffect); } // 如果 shouldTrack 为 true,说明当前的依赖是新的依赖,需要收集 if (shouldTrack) { // 记录当前的依赖 dep.add(activeEffect); // activeEffect.deps 记录的维度不同,稍后会讲到 activeEffect.deps.push(dep); // 开发环境下才会用到 if (activeEffect.onTrack) { activeEffect.onTrack(Object.assign({ effect: activeEffect }, debuggerEventExtraInfo)); } } }
这里设计很妙,shouldTrack
没啥好介绍的,重要的是shouldTrack
的值是如何确定的,这里有两种情况:
effectTrackDepth
小于maxMarkerBits
,这种情况代表当前的依赖收集的深度还没有达到最大值,这里的深度指的就是递归调用的深度,也可以是effect
的嵌套调用的深度,但是通常没人会手写嵌套调用到溢出;effectTrackDepth
大于maxMarkerBits
,这种情况代表当前的依赖收集的深度已经达到最大值,就判断当前的依赖是否已经被收集过了,这里的相同依赖指的就是相同的effect
。
在赖收集的深度还没有到达最大值的情况下,会有两个方法来判断当前的依赖是否是新的依赖,newTracked
和wasTracked
的实现如下:
function newTracked(dep) { return (dep.n & trackOpBit) === 0; } function wasTracked(dep) { return (dep.n & trackOpBit) > 0; }
这里使用的是位运算,trackOpBit
的值是由effectTrackDepth
确定的,在effect
执行的过程中,effectTrackDepth
会进行自增,当effect
执行完成之后,effectTrackDepth
会进行自减;
下面就是trackOpBit
值的确定过程,在effect
的run
方法中,会有如下代码:
function run() { // ... try { // ... // 在执行 effect 中会对 trackOpBit 进行赋值 // 1 << ++effectTrackDepth 是位移运算,是将 1 左移 effectTrackDepth 位 // 这里可以将 1 理解为二进制数据,左移一位就是在二进制数据的最后面添加一个 0 // 例如: // 1 << 0 = 1 = 0001 // 1 << 1 = 2 = 0010 // 1 << 2 = 4 = 0100 // 1 << 10 = 1024 = 10000000000 // 可自行在控制台进行验证,这里 effectTrackDepth 最大值是 30,由 maxMarkerBits 决定 trackOpBit = 1 << ++effectTrackDepth; // 控制深度 if (effectTrackDepth <= maxMarkerBits) { initDepMarkers(this); } else { cleanupEffect(this); } // 执行回调 return this.fn(); } finally { // 执行完毕确定当前的依赖收集的深度标记 if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this); } // effectTrackDepth 自减,空出位置 trackOpBit = 1 << --effectTrackDepth; // ... } }
trackOpBit
的值最终会映射到dep.n
和dep.w
上,这两个值在上面已经出现过了,现在来详细扒一扒,在上面的代码中,可以看到有两个方法initDepMarkers
和finalizeDepMarkers
,这两个方法的实现如下:
const initDepMarkers = ({ deps }) => { if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].w |= trackOpBit; // set was tracked } } }; const finalizeDepMarkers = (effect) => { const { deps } = effect; if (deps.length) { let ptr = 0; for (let i = 0; i < deps.length; i++) { const dep = deps[i]; if (wasTracked(dep) && !newTracked(dep)) { dep.delete(effect); } else { deps[ptr++] = dep; } // clear bits dep.w &= ~trackOpBit; dep.n &= ~trackOpBit; } deps.length = ptr; } };
这个东西到底有啥用呢?可以看到的是最终trackOpBit
的值会被赋值到dep.w
和dep.n
上,dep.w
的作用是用来记录当前的依赖是否被收集过的,dep.n
的作用是用来记录当前的依赖收集的深度的;
到这里其实整个流程已经串起来了,画个图来说明一下:
这个图忽略很多细节,但是可以看到整个流程是怎么走的,到这里我们回顾一下整个流程:
- 我们定义一个响应式对象,这个响应式对象会拦截你对属性的操作
- 当我们在
effect
中访问响应式对象的属性时,会触发get
方法,这个时候会进行依赖收集 effect
在执行的过程中,首先会将全局的activeEffect
设置为当前的effect
,effect
就是依赖收集中的依赖effect
在执行的过程中,会标记嵌套深度,这个可以防止effect
的嵌套过深,导致内存溢出effect
在执行的过程中,会将trackOpBit
的值赋值到dep.w
和dep.n
上,这个值会记录当前的依赖是否被收集过,以及当前的依赖收集的深度effect
最后会执行用户传入的回调,这个回调就是我们在effect
中传入的函数effect
执行完毕,会将effectTrackDepth
的值自减,相当于减少层级的深度- 当我们修改响应式对象的属性时,会触发
set
方法,这个时候会执行effect
- 这个时候就会回到第3步
自此整个流程就串起来了,我们已经知道了依赖是如何收集的,依赖收集的细节就是响应式对象的访问一定要在effect
中,这样才能收集到依赖;
如果不在effect
中访问响应式对象的属性,那么在track
过程中是不存在activeEffect
的,所以就不会进行依赖收集;
依赖触发
上面已经完整的了解到了依赖收集的流程,那么依赖触发的流程是怎么样的呢?我们先来看一下trigger
方法的实现:
function trigger(target, type, key, newValue, oldValue, oldTarget) { const depsMap = targetMap.get(target); if (!depsMap) { // never been tracked return; } let deps = []; if (type === "clear" /* TriggerOpTypes.CLEAR */) { // collection being cleared // trigger all effects for target deps = [...depsMap.values()]; } else if (key === 'length' && isArray(target)) { const newLength = Number(newValue); depsMap.forEach((dep, key) => { if (key === 'length' || key >= newLength) { deps.push(dep); } }); } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { deps.push(depsMap.get(key)); } // also run for iteration key on ADD | DELETE | Map.SET switch (type) { case "add" /* 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 "delete" /* TriggerOpTypes.DELETE */: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)); if (isMap(target)) { deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)); } } break; case "set" /* TriggerOpTypes.SET */: if (isMap(target)) { deps.push(depsMap.get(ITERATE_KEY)); } break; } } const eventInfo = (process.env.NODE_ENV !== 'production') ? { target, type, key, newValue, oldValue, oldTarget } : undefined; if (deps.length === 1) { if (deps[0]) { if ((process.env.NODE_ENV !== 'production')) { triggerEffects(deps[0], eventInfo); } else { triggerEffects(deps[0]); } } } else { const effects = []; for (const dep of deps) { if (dep) { effects.push(...dep); } } if ((process.env.NODE_ENV !== 'production')) { triggerEffects(createDep(effects), eventInfo); } else { triggerEffects(createDep(effects)); } } }
代码量比较多,拆解来看,先看参数:
/** * @param target 当前响应式对象 * @param type 触发类型 * @param key 属性名 * @param newValue 新值 * @param oldValue 旧值 * @param oldTarget 旧的响应式对象 */ function trigger(target, type, key, newValue, oldValue, oldTarget) { }
这些参数的大概作用就是用来确定要执行哪些effect
,接着往下看:
// 省略 trigger 的定义 // 获取当前响应式对象的依赖 const depsMap = targetMap.get(target); if (!depsMap) { // never been tracked return; }
targetMap
在上面已经介绍过了,targetMap
会将当前响应式对象作为key
,依赖作为value
存储起来,所以这里通过target
就可以获取到当前响应式对象的依赖;
接着往下看:
// 最终要执行的 effect 都会存储在 deps 中 let deps = []; if (type === "clear" /* TriggerOpTypes.CLEAR */) { // ... } else if (key === 'length' && isArray(target)) { // ... } else { // ... }
deps
是一个数组,最终要执行的effect
都会存储在deps
中,这里会根据type
和key
的不同,将要执行的effect
存储到deps
中;
tpye
是操作类型,key
是当前操作的属性名,这里可以看到开始会有两个判断,一个是type='clear'
,一个是key='length'
;
这两个暂时先不管,先看else
中的代码:
// 如果 key 不为 undefined,将 key 对应的依赖存储到 deps 中 if (key !== void 0) { deps.push(depsMap.get(key)); }
这一步就将key
对应的依赖存储到deps
中,例如:
const obj = reactive({ name: '田八', age: 18 }) effect(() => { console.log(obj.name) }) effect(() => { console.log(obj.name) })
这里name
有两个effect
依赖,所以到这一步的时候,deps
中就会有两个effect
;
接着往下看:
// 根据 type 的不同,将对应的依赖存储到 deps 中 switch (type) { // 如果 type 为 add case "add" /* TriggerOpTypes.ADD */: // 如果 target 不是数组 if (!isArray(target)) { // 将 ITERATE_KEY 对应的依赖存储到 deps 中 deps.push(depsMap.get(ITERATE_KEY)); // 如果 target 是 Map if (isMap(target)) { // 将 MAP_KEY_ITERATE_KEY 对应的依赖存储到 deps 中 deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)); } } // 如果 key 是数字 else if (isIntegerKey(key)) { // new index added to array -> length changes // 将 length 对应的依赖存储到 deps 中 deps.push(depsMap.get('length')); } break; // 如果 type 为 delete case "delete" /* TriggerOpTypes.DELETE */: // 如果 target 不是数组 if (!isArray(target)) { // 将 ITERATE_KEY 对应的依赖存储到 deps 中 deps.push(depsMap.get(ITERATE_KEY)); // 如果 target 是 Map if (isMap(target)) { // 将 MAP_KEY_ITERATE_KEY 对应的依赖存储到 deps 中 deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)); } } break; // 如果 type 为 set case "set" /* TriggerOpTypes.SET */: // 如果 target 是 Map if (isMap(target)) { // 将 MAP_KEY_ITERATE_KEY 对应的依赖存储到 deps 中 deps.push(depsMap.get(ITERATE_KEY)); } break; }
这里的信息量就比较大了,这里会根据type
的不同,将对应的依赖存储到deps
中;
这里的type
有三种,分别是add
、delete
、set
;
这里的add
和delete
是对数组和Map
的操作,set
是对Map
的操作;
这里的add
和delete
都会将ITERATE_KEY
对应的依赖存储到deps
中,如果是Map
,还会将MAP_KEY_ITERATE_KEY
对应的依赖存储到deps
中;
ITERATE_KEY
和MAP_KEY_ITERATE_KEY
见名知意,就是对迭代器的依赖;
思考一个问题,如果对数组执行push
或者其他修改数组长度的操作,那么如何触发类似于forEarch
、map
等迭代器的依赖呢?
这里就是通过ITERATE_KEY
和MAP_KEY_ITERATE_KEY
来实现的,这里会将ITERATE_KEY
和MAP_KEY_ITERATE_KEY
对应的依赖存储到deps
中,这样就可以触发迭代器的依赖了;
至于ITERATE_KEY
和MAP_KEY_ITERATE_KEY
是什么时候加入到depsMap
中的,这里在之前的章节中都有出现,但是没有深入讲解,这里建议大家可以回顾一下之前的章节,并且自己也跟着在源码中找一下,这样才能更好的理解;
接着往下看:
// 删除开发环境下才会执行的代码 // 如果 deps 中有依赖,并且只有一个 if (deps.length === 1) { if (deps[0]) { // 直接调用 triggerEffects 触发依赖 triggerEffects(deps[0]); } } else { // 如果 deps 中有多个依赖,将多个依赖合并到一个数组中 const effects = []; for (const dep of deps) { if (dep) { effects.push(...dep); } } // 调用 triggerEffects 触发依赖,会有深度检测 triggerEffects(createDep(effects)); }
这里的逻辑就比较简单了,如果deps
中只有一个依赖,那么直接调用triggerEffects
触发依赖;
如果deps
中有多个依赖,那么将多个依赖合并到一个数组中,然后调用triggerEffects
触发依赖,createDep
就是为当前的依赖添加w
和n
属性,这个就是上面讲到过的;
然后就是triggerEffects
了,这个函数就是用来触发依赖的:
function triggerEffects(dep, debuggerEventExtraInfo) { // spread into array for stabilization // 将 dep 转换为数组 const effects = isArray(dep) ? dep : [...dep]; // 先执行 computed 依赖 for (const effect of effects) { if (effect.computed) { triggerEffect(effect, debuggerEventExtraInfo); } } // 再执行非 computed 依赖 for (const effect of effects) { if (!effect.computed) { triggerEffect(effect, debuggerEventExtraInfo); } } }
这里没有什么特别的,就是先执行computed
依赖,再执行非computed
依赖,主要是为了保证computed
依赖的执行顺序;
先执行computed
依赖的目的是为了防止后面非computed
依赖的执行,可能会修改computed
依赖的值,这样就会导致computed
依赖的值不正确;
再来看triggerEffect
:
// 删除开发环境下才会执行的代码 function triggerEffect(effect, debuggerEventExtraInfo) { // 如果 effect 不是 activeEffect 或者 effect 允许递归 if (effect !== activeEffect || effect.allowRecurse) { // 如果 effect 有调度器,就执行调度器 if (effect.scheduler) { effect.scheduler(); } // 否则就直接执行 else { effect.run(); } } }
这里就非常好理解了,最开头的判断是为了防止死循环,如果effect
是activeEffect
,那么就不会执行effect
;
最终执行effect
的逻辑就是判断effect
是否有scheduler
,如果有就执行scheduler
,否则就直接执行effect
,调度器这一块在之前的章节中也有讲到;
总结
到此为止,我们总算将Vue3
的响应式原理讲完了,响应式核心我总共分了 4 篇文章来讲解,这里我再简单的总结一下:
第一篇Vue3 的响应式核心 reactive 和 effect 实现原理以及源码分析:介绍了Vue3
的响应式原理,以及Vue3
的响应式原理是如何实现的,这一篇是整体将响应式核心过了一遍,但是并不深入;
第二篇跟着 Vue3 的源码学习 reactive 背后的实现原理:详细的介绍了Vue3
的响应式拦截是如何实现的,主要介绍Vue3
是如何对可观察对象进行拦截的,例如Object
、Array
、Map
、Set
等;
第三篇Vue3 的依赖收集,这里的依赖指代的是什么?:主要介绍了effect
的调度器是如何使用的,已经effect
一些杂项的东西;
然后就是本篇,在之前几篇的铺垫下,这篇主要是将之前的内容串在一起,这样就构成了一个完整的响应式核心的流程;
响应式原理分开来看就是我写的第二篇和第三篇,响应式首先是响应式拦截,这一块就是reactive
的核心,然后是依赖收集,这一块就是effect
的核心;
最后将这两个核心串在一起,就是响应式核心,这样就构成了一个完整的响应式核心的流程;