前几天写了一篇关于Vue 3.0 reactive API 源码实现的文章,发现你们仍是蛮有兴趣对于源码这一块的。阅读的人数虽然很少,可是 200
屡次阅读,仍是阔以的!而且,在当时阿里的一位前辈也指出了文章存在的不足,就是没有分析 Proxy
是如何配合 Effect
实现响应式的原理,即依赖收集和派发更新的过程。javascript
因此,此次咱们就来完全了解一下,Vue 3.0
依赖收集和派发更新的整个过程。vue
值得一提的是在Vue 3.0
中没有了watcher
的概念,取而代之的是effect
,因此接下来会接触不少和effect
相关的函数
在文章的开始前,咱们先准备这样一个简单的 case
,以便后续分析具体逻辑:java
main.js 项目入口node
import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')
App.vue 组件react
<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>
首先,咱们你们都知道在一般状况下,咱们的页面会使用当前实例的一些属性、计算属性、方法等等。因此,在组件渲染的过程就会发生依赖收集的这个过程。也所以,咱们先从组件的渲染过程开始分析。数组
在组件的渲染过程当中,会安装(建立)一个渲染 reactive effect
,即 Vue 3.0
在编译 template
的时候,对是否有订阅数据作出相应的判断,建立对应的渲染 reactive 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()
。它传入几个参数,它们分别为:app
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()
函数定义:post
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()
函数。因此,接下来,咱们就正式进入依赖收集部分的分析。
前面,咱们已经讲到了在组件渲染过程会安装渲染 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
的过程会分为两个阶段:
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; }; }
两个阶段的不一样点在于,第一阶段的target
为一个object
(即上面所说的toRefs
的对象结构),而第二阶段的target
为普通对象{count: 0}
。具体细节能够看我 上篇文章第一阶段:触发普通对象的
get
因为此时是第一阶段,因此咱们会命中 isRef()
的逻辑,并返回 res.value
。此时就会触发 reactive
定义的 Proxy
对象的 get
。而且须要注意的是 toRefs()
只能用于对象,不然咱们即时触发了 get
也不能获取对应的值(这其实也是看源码的一些好处,深度理解 API
的使用)。
第二阶段:触发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) { debugger 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
数组中,即订阅主题。从而,造成整个依赖收集过程。
分析完依赖收集的过程,那么派发更新的整个过程的分析也将会水到渠成。首先,对应派发更新,是指当某个主题发生变化时,在咱们这个 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()
函数的定义:
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()
等等
虽然,整个依赖收集的过程我足足花费了 9 个小时来总结分析,而且整个文章的内容也达到了 4k+ 字。可是,这并不表明了它很复杂。其实整个依赖收集和派发更新的过程,仍是很是简单明了的。首先定义全局的渲染 effect()
,而后在 get()
中调用 track()
进行依赖收集。接下来,若是依赖发生变化,就会走派发更新的流程,先更新依赖的值,而后调用 trigger()
收集 effect()
,在下一个 tick
中执行 effect()
,最后更新组件。
写做不易,若是你以为有收获的话,能够帅气三连击!!!