引言
前几天写了一篇关于Vue 3.0 reactive API 源码实现的文章,发现大家还是蛮有兴趣对于源码这一块的。阅读的人数虽然不多,不过还是阔以的!但是,在上篇文章中是没有分析 Proxy
是如何配合 Effect
实现响应式的原理,即依赖收集和派发更新的过程。
所以,这次我们就来彻底了解一下,Vue 3.0
依赖收集和派发更新的整个过程。
值得一提的是在Vue 3.0
中没有了watcher
的概念,取而代之的是effect
,所以接下来会接触很多和effect
相关的函数
一、开始前准备
在文章的开始前,我们先准备这样一个简单的 case
,以便后续分析具体逻辑:
main.js 项目入口
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
App.vue 组件
<template>
<button @click="inc">Clicked {{ count }} times.</button>
</template>
<script>
import { reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({
count: 0,
})
const inc = () => {
state.count++
}
return {
inc,
...toRefs(state)
}
}
}
</script>
二、安装渲染 Effect
首先,我们大家都知道在通常情况下,我们的页面会使用当前实例的一些属性、计算属性、方法等等。所以,在组件渲染的过程就会发生依赖收集的这个过程。也因此,我们先从组件的渲染过程开始分析。
在组件的渲染过程中,会安装(创建)一个渲染 effect
,即 Vue 3.0
在编译 template
的时候,对是否有订阅数据做出相应的判断,创建对应的渲染 effect
,它的定义如下:
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {
....
instance.isMounted = true;
}
else {
...
}
}, (process.env.NODE_ENV !== 'production') ? createDevEffectOptions(instance) : prodEffectOptions);
};
我们来大致分析一下 setupRenderEffect()
。它传入几个参数,它们分别为:
instance
当前vm
实例initialVNode
可以是组件VNode
或者普通VNode
container
挂载的模板,例如div#app
对应的节点anchor
,parentSuspense
,isSVG
普通情况下都为null
然后在当前实例 instance
上创建属性 update
赋值为 effect()
函数的执行结果,effect()
函数传入两个参数:
componentEffect()
函数,它会在具体逻辑之后提到,这里我们先不讲createDevEffectOptions(instance)
用于后续的派发更新,它会返回一个对象:{ scheduler: queueJob(job) { if (!queue.includes(job)) { queue.push(job); queueFlush(); } }, onTrack: instance.rtc ? e => invokeHooks(instance.rtc, e) : void 0, onTrigger: instance.rtg ? e => invokeHooks(instance.rtg, e) : void 0 }
然后,我们再来看看effect()
函数定义:
function effect(fn, options = EMPTY_OBJ) {
if (isEffect(fn)) {
fn = fn.raw;
}
const effect = createReactiveEffect(fn, options);
if (!options.lazy) {
effect();
}
return effect;
}
effect()
函数的逻辑较为简单,首先判断是否已经为 effect
,是则取出之前定义的。不是则通过 ceateReactiveEffect()
创建一个 effect
,而 creatReactiveEffect()
的逻辑会是这样:
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect(...args) {
return run(effect, fn, args);
};
effect._isEffect = true;
effect.active = true;
effect.raw = fn;
effect.deps = [];
effect.options = options;
return effect;
}
可以看到在 createReactiveEffect()
中先定义了一个 reactiveEffect()
函数赋值给 effect
,它又调用了 run()
方法。而 run()
方法中传入三个参数,分别为:
effect
,即reactiveEffect()
函数本身fn
,即在刚开始instance.update
是调用effect
函数时,传入的函数componentEffect()
args
为一个空数组
并且,对 effect
进行了一些初始化,例如我们最熟悉的 Vue 2x
中的 deps
就出现在 effect
这个对象上。
然后,我们分析一下 run()
函数的逻辑:
function run(effect, fn, args) {
if (!effect.active) {
return fn(...args);
}
if (!effectStack.includes(effect)) {
cleanup(effect);
try {
enableTracking();
effectStack.push(effect);
activeEffect = effect;
return fn(...args);
}
finally {
effectStack.pop();
resetTracking();
activeEffect = effectStack[effectStack.length - 1];
}
}
}
在这里,初次创建 effect
,我们会命中第二个分支逻辑,即当前 effectStack
栈中不包含这个 effect
。那么,首先会执行 cleanup(effect)
,即遍历effect.deps
,清空之前的依赖。
cleanup()
的逻辑其实在Vue 2x
的源码中也有的,避免依赖的重复收集。并且,对比Vue 2x
,Vue 3.0
中的track
其实相当于watcher
,在track
中会进行依赖的收集,后面我们会讲track
的具体实现
然后,执行enableTracking()
和effectStack.push(effect)
,前者的逻辑很简单,即可以追踪,用于后续触发 track
的判断:
function enableTracking() {
trackStack.push(shouldTrack);
shouldTrack = true;
}
而后者,即将当前的 effect
添加到 effectStack
栈中。最后,执行 fn()
,即我们一开始定义的 instance.update = effect()
时候传入的 componentEffect()
:
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
const subTree = (instance.subTree = renderComponentRoot(instance));
// beforeMount hook
if (instance.bm !== null) {
invokeHooks(instance.bm);
}
if (initialVNode.el && hydrateNode) {
// vnode has adopted host node - perform hydration instead of mount.
hydrateNode(initialVNode.el, subTree, instance, parentSuspense);
}
else {
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
initialVNode.el = subTree.el;
}
// mounted hook
if (instance.m !== null) {
queuePostRenderEffect(instance.m, parentSuspense);
}
// activated hook for keep-alive roots.
if (instance.a !== null &&
instance.vnode.shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */) {
queuePostRenderEffect(instance.a, parentSuspense);
}
instance.isMounted = true;
}
else {
...
}
}, (process.env.NODE_ENV !== 'production') ? createDevEffectOptions(instance) : prodEffectOptions);
而接下来就会进入组件的渲染过程,其中涉及renderComponnetRoot
、patch
等等,这次我们并不会分析组件渲染具体细节。
安装渲染 Effect
,是为后续的依赖收集做一个前期的准备。因为在后面会用到 setupRenderEffect
中定义的 effect()
函数,以及会调用 run()
函数。所以,接下来,我们就正式进入依赖收集部分的分析。
三、依赖收集
get
前面,我们已经讲到了在组件渲染过程会安装渲染 Effect
。然后,进入渲染组件的阶段,即 renderComponentRoot()
,而此时会调用 proxyToUse
,即会触发 runtimeCompiledRenderProxyHandlers
的 get
,即:
get(target, key) {
...
else if (renderContext !== EMPTY_OBJ && hasOwn(renderContext, key)) {
accessCache[key] = 1 /* CONTEXT */;
return renderContext[key];
}
...
}
可以看出,此时会命中 accessCache[key] = 1
和 renderContext[key]
。对于前者是做一个缓存的作用,后者是从当前的渲染上下文中获取 key
对应的值((对于本文这个 case
,key
对应的就是 count
,它的值为 0
)。
那么,我想这个时候大家会立即反应,此时会触发这个 count
对应 Proxy
的 get
。但是,在我们这个 case
中,用了 toRefs()
将 reactive
包裹导出,所以这个触发 get
的过程会分为两个阶段:
两个阶段的不同点在于,第一阶段的target
为一个object
(即上面所说的toRefs
的对象结构),而第二阶段的target
为Proxy
对象{count: 0}
。具体细节可以看我上篇文章
Proxy
对象toRefs()
后得到对象的结构:
{
value: 0
_isRef: true
get: function() {}
set: ƒunction(newVal) {}
}
我们先来看看 get()
的逻辑:
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
...
const res = Reflect.get(target, key, receiver);
if (isSymbol(key) && builtInSymbols.has(key)) {
return res;
}
...
// ref unwrapping, only for Objects, not for Arrays.
if (isRef(res) && !isArray(target)) {
return res.value;
}
track(target, "get" /* GET */, key);
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res;
};
}
第一阶段:触发普通对象的 get
由于此时是第一阶段,所以我们会命中 isRef()
的逻辑,并返回 res.value
。此时就会触发 reactive
定义的 Proxy
对象的 get
。并且需要注意的是 toRefs()
只能用于对象,否则我们即时触发了 get
也不能获取对应的值(这其实也是看源码的一些好处,深度理解 API
的使用)。
track
第二阶段:触发Proxy
对象的get
此时属于第二阶段,所以我们会命中 get
的最后逻辑:
track(target, "get" /* GET */, key);
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res;
可以看到,首先会调用 track()
函数,进行依赖收集,而 track()
函数定义如下:
function track(target, type, key) {
if (!shouldTrack || activeEffect === undefined) {
return;
}
let depsMap = targetMap.get(target);
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (dep === void 0) {
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if ((process.env.NODE_ENV !== 'production') && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
});
}
}
}
可以看到,第一个分支逻辑不会命中,因为我们在前面分析 run()
的时候,就已经定义 ishouldTrack = true
和 activeEffect = effect
。然后,命中 depsMap === void 0
逻辑,往 targetMap
中添加一个键名为 {count: 0}
键值为一个空的 Map
:
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()));
}
而此时,我们也可以对比Vue 2.x
,这个{count: 0}
其实就相当于data
选项(以下统称为data
)。所以,这里也可以理解成先对data
初始化一个Map
,显然这个Map
中存的就是不同属性对应的dep
然后,对 count
属性初始化一个 Map
插入到 data
选项中,即:
let dep = depsMap.get(key);
if (dep === void 0) {
depsMap.set(key, (dep = new Set()));
}
所以,此时的 dep
就是 count
属性对应的主题对象了。接下来,则判断是否当前 activeEffect
存在于 count
的主题中,如果不存在则往主题 dep
中添加 activeEffect
,并且将当前主题 dep
添加到 activeEffect
的 deps
数组中。
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
// 最后的分支逻辑,我们这次并不会命中
}
最后,再回到 get()
,会返回 res
的值,在我们这个 case
是 res
的值是 0
。
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res;
总结
好了,整个 reactive
的依赖收集过程,已经分析完了。我们再来回忆其中几个关键点,首先在组件渲染过程,会给当前 vm
实例创建一个 effect
,然后将当前的 activeEffect
赋值为 effect
,并在 effect
上创建一些属性,例如非常重要的 deps
用于保存依赖。
接下来,当该组件使用了 data
中的变量时,会访问对应变量的 get()
。第一次访问 get()
会创建 data
对应的 depsMap
,即 targetMap
。然后再往 targetMap
的 depMap
中添加对应属性的 Map
,即 depsMap
。
创建完属性的 depsMap
后,一方面会往该属性的 depsMap
中添加当前 activeEffect
,即收集订阅者。另一方面,将该属性的 depsMap
添加到 activeEffect
的 deps
数组中,即订阅主题。从而,形成整个依赖收集过程。
整个 get
过程的流程图
四、派发更新
set
分析完依赖收集的过程,那么派发更新的整个过程的分析也将会水到渠成。首先,对应派发更新,是指当某个主题发生变化时,在我们这个 case
是当 count
发生变化时,此时会触发 data
的 set()
,即 target
为 data
,key
为 count
。
function set(target, key, value, receiver) {
...
const oldValue = target[key];
if (!shallow) {
value = toRaw(value);
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
}
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, "add" /* ADD */, key, value);
}
else if (hasChanged(value, oldValue)) {
trigger(target, "set" /* SET */, key, value, oldValue);
}
}
return result;
};
可以看到,oldValue
为 0
,而我们的 shallow
此时为 false
,value
为 1。那么,我们看一下 toRaw()
函数的逻辑:
function toRaw(observed) {
return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed;
}
toRaw()
中有两个 WeakMap
类型的变量 reactiveToRaw
和 readonlyRaw
。前者是在初始化 reactive
的时候,将对应的 Proxy
对象存入 reactiveToRaw
这个 Map
中。后者,则是存入和前者相反的键值对。即:
function createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
...
observed = new Proxy(target, handlers);
toProxy.set(target, observed);
toRaw.set(observed, target);
...
}
很显然对于 toRaw()
方法而言,会返回 observer
即 1。所以,回到 set()
的逻辑,调用 Reflect.set()
方法将 data
上的 count
的值修改为 1。并且,接下来我们还会命中 target === toRaw(receiver)
的逻辑。
而 target === toRaw(receiver)
的逻辑会处理两个逻辑:
- 如果当前对象不存在该属性,触发
triger()
函数对应的add
。 - 或者该属性发生变化,触发
triger()
函数对应的set
trigger
首先,我们先看一下 trigger()
函数的定义:
function trigger(target, type, key, newValue, oldValue, oldTarget) {
const depsMap = targetMap.get(target);
if (depsMap === void 0) {
// never been tracked
return;
}
const effects = new Set();
const computedRunners = new Set();
if (type === "clear" /* CLEAR */) {
...
}
else if (key === 'length' && isArray(target)) {
...
}
else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key));
}
// also run for iteration key on ADD | DELETE | Map.SET
if (type === "add" /* ADD */ ||
(type === "delete" /* DELETE */ && !isArray(target)) ||
(type === "set" /* SET */ && target instanceof Map)) {
const iterationKey = isArray(target) ? 'length' : ITERATE_KEY;
addRunners(effects, computedRunners, depsMap.get(iterationKey));
}
}
const run = (effect) => {
scheduleRun(effect, target, type, key, (process.env.NODE_ENV !== 'production')
? {
newValue,
oldValue,
oldTarget
}
: undefined);
};
// Important: computed effects must be run first so that computed getters
// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run);
effects.forEach(run);
}
并且,大家可以看到这里有一个细节,就是计算属性的派发更新要优先于普通属性。
在 trigger()
函数,首先获取当前 targetMap
中 data
对应的主题对象的 depsMap
,而这个 depsMap
即我们在依赖收集时在 track
中定义的。
然后,初始化两个 Set
集合 effects
和 computedRunners
,用于记录普通属性或计算属性的 effect
,这个过程是会在 addRunners()
中进行。
接下来,定义了一个 run()
函数,包裹了 scheduleRun()
函数,并对开发环境和生产环境进行不同参数的传递,这里由于我们处于开发环境,所以传入的是一个对象,即:
{
newValue: 1,
oldValue: 0,
oldTarget: undefined
}
然后遍历 effects
,调用 run()
函数,而这个过程实际调用的是 scheduleRun()
:
function scheduleRun(effect, target, type, key, extraInfo) {
if ((process.env.NODE_ENV !== 'production') && effect.options.onTrigger) {
const event = {
effect,
target,
key,
type
};
effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event);
}
if (effect.options.scheduler !== void 0) {
effect.options.scheduler(effect);
}
else {
effect();
}
}
此时,我们会命中 effect.options.scheduler !== void 0
的逻辑。然后,调用 effect.options.scheduler()
函数,即调用 queueJob()
函数:
scheduler
这个属性是在setupRenderEffect
调用effect
函数时创建的。
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job);
queueFlush();
}
}
这里使用了一个队列维护所有effect()
函数,其实也和Vue 2x
相似,因为我们effect()
相当于watcher
,而Vue 2x
中对watcher
的调用也是通过队列的方式维护。队列的存在具体是为了保持watcher
触发的次序,例如先父watcher
后子watcher
。
可以看到 我们会先将 effect()
函数添加到队列 queue
中,然后调用 queueFlush()
清空和调用 queue
:
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true;
nextTick(flushJobs);
}
}
熟悉 Vue 2x
源码的同学,应该知道 Vue 2x
中的 watcher
也是在下一个 tick
中执行,而 Vue 3.0
也是一样。而 flushJobs
中就会对 queue
队列中的 effect()
进行执行:
function flushJobs(seen) {
isFlushPending = false;
isFlushing = true;
let job;
if ((process.env.NODE_ENV !== 'production')) {
seen = seen || new Map();
}
while ((job = queue.shift()) !== undefined) {
if (job === null) {
continue;
}
if ((process.env.NODE_ENV !== 'production')) {
checkRecursiveUpdates(seen, job);
}
callWithErrorHandling(job, null, 12 /* SCHEDULER */);
}
flushPostFlushCbs(seen);
isFlushing = false;
if (queue.length || postFlushCbs.length) {
flushJobs(seen);
}
}
flushJob()
主要会做几件事:
- 首先初始化一个
Map
集合seen
,然后在递归queue
队列的过程,调用checkRecursiveUpdates()
记录该job
即effect()
触发的次数。如果超过100
次会抛出错误。 然后调用
callWithErrorHandling()
,执行job
即effect()
,而我们都知道的是这个effect
是在createReactiveEffect()
时创建的reactiveEffect()
,所以,最终会执行run()
方法,即执行最初在setupRenderEffectect
定义的effect()
:const setupRenderEffectect = (instance, initialVNode, container, anchor, parentSuspense, isSVG) => { // create reactive effect for rendering instance.update = effect(function componentEffect() { if (!instance.isMounted) { ... } else { ... const nextTree = renderComponentRoot(instance); const prevTree = instance.subTree; instance.subTree = nextTree; if (instance.bu !== null) { invokeHooks(instance.bu); } if (instance.refs !== EMPTY_OBJ) { instance.refs = {}; } patch(prevTree, nextTree, hostParentNode(prevTree.el), getNextHostNode(prevTree), instance, parentSuspense, isSVG); instance.vnode.el = nextTree.el; if (next === null) { updateHOCHostEl(instance, nextTree.el); } if (instance.u !== null) { queuePostRenderEffect(instance.u, parentSuspense); } if ((process.env.NODE_ENV !== 'production')) { popWarningContext(); } } }, (process.env.NODE_ENV !== 'production') ? createDevEffectOptions(instance) : prodEffectOptions); };
即此时就是派发更新的最后阶段了,会先
renderComponentRoot()
创建组件VNode
,然后patch()
,即走一遍组件渲染的过程(当然此时称为更新更为贴切)。从而,完成视图的更新。
总结
同样地,我们也来回忆派发更新过程的几个关键点。首先,触发依赖的 set()
,它会调用 Reflect.set()
修改依赖对应属性的值。然后,调用 trigger()
函数,获取 targetMap
中对应属性的主题,即 depsMap()
,并且将 depsMap
中的 effect()
存进 effect
集合中。接下来,就将 effect
进队,在下一个 tick
中清空和执行所有 effect
。最后,和在初始化的时候提及的一样,走组件的更新过程,即 renderComponent()
、patch()
等等。
整个 set
过程的流程图
结语
虽然,整个依赖收集的过程我足足花费了 9 个小时来总结分析,并且整个文章的内容也达到了 4k+ 字。但是,这并不代表了它很复杂。其实整个依赖收集和派发更新的过程,还是非常简单明了的。首先定义全局的渲染 effect()
,然后在 get()
中调用 track()
进行依赖收集。接下来,如果依赖发生变化,就会走派发更新的流程,先更新依赖的值,然后调用 trigger()
收集 effect()
,在下一个 tick
中执行 effect()
,最后更新组件。
写作不易,如果你觉得有收获的话,可以帅气三连击!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。