前幾天寫了一篇關於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()
,最後更新組件。
寫做不易,若是你以爲有收穫的話,能夠帥氣三連擊!!!