分析 Vue 3.0 響應式原理


引言

前幾天寫了一篇關於Vue 3.0 reactive API 源碼實現的文章,發現大家還是蠻有興趣對於源碼這一塊的。閱讀的人數雖然不多,但是 200 多次閱讀,還是闊以的!並且,在當時阿里的一位前輩也指出了文章存在的不足,就是沒有分析 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

首先,我們大家都知道在通常情況下,我們的頁面會使用當前實例的一些屬性、計算屬性、方法等等。所以,在組件渲染的過程就會發生依賴收集的這個過程。也因此,我們先從組件的渲染過程開始分析。

在組件的渲染過程中,會安裝(創建)一個渲染 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()。它傳入幾個參數,它們分別為:

  • 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 的過程會分為兩個階段:

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 的使用)。

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) { 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 數組中,即訂閱主題。從而,形成整個依賴收集過程。

電腦刺綉綉花廠 http://www.szhdn.com 廣州品牌設計公司https://www.houdianzi.com

四、派發更新

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() 等等


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM