4k+ 字分析 Vue 3.0 響應式原理(依賴收集和派發更新)

引言

前幾天寫了一篇關於Vue 3.0 reactive API 源碼實現的文章,發現你們仍是蠻有興趣對於源碼這一塊的。閱讀的人數雖然很少,可是 200 屢次閱讀,仍是闊以的!而且,在當時阿里的一位前輩也指出了文章存在的不足,就是沒有分析 Proxy 是如何配合 Effect 實現響應式的原理,即依賴收集和派發更新的過程。javascript

因此,此次咱們就來完全瞭解一下,Vue 3.0 依賴收集和派發更新的整個過程。vue

值得一提的是在 Vue 3.0 中沒有了 watcher 的概念,取而代之的是 effect ,因此接下來會接觸不少和 effect 相關的函數

1、開始前準備

在文章的開始前,咱們先準備這樣一個簡單的 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>

2、安裝渲染 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()。它傳入幾個參數,它們分別爲: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 2xVue 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);
而接下來就會進入組件的渲染過程,其中涉及 renderComponnetRootpatch 等等,此次咱們並不會分析組件渲染具體細節。

安裝渲染 Effect,是爲後續的依賴收集作一個前期的準備。由於在後面會用到 setupRenderEffect 中定義的 effect() 函數,以及會調用 run() 函數。因此,接下來,咱們就正式進入依賴收集部分的分析。

3、依賴收集

get

前面,咱們已經講到了在組件渲染過程會安裝渲染 Effect。而後,進入渲染組件的階段,即 renderComponentRoot(),而此時會調用 proxyToUse,即會觸發 runtimeCompiledRenderProxyHandlersget,即:

get(target, key) {
    ...
    else if (renderContext !== EMPTY_OBJ && hasOwn(renderContext, key)) {
        accessCache[key] = 1 /* CONTEXT */;
        return renderContext[key];
    }
    ...
}

能夠看出,此時會命中 accessCache[key] = 1renderContext[key] 。對於前者是作一個緩存的做用,後者是從當前的渲染上下文中獲取 key 對應的值((對於本文這個 casekey 對應的就是 count,它的值爲 0)。

那麼,我想這個時候你們會當即反應,此時會觸發這個 count 對應 Proxyget。可是,在咱們這個 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 = trueactiveEffect = 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 添加到 activeEffectdeps 數組中。

if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
    // 最後的分支邏輯,咱們此次並不會命中
}

最後,再回到 get(),會返回 res 的值,在咱們這個 caseres 的值是 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。而後再往 targetMapdepMap 中添加對應屬性的 Map,即 depsMap

建立完屬性的 depsMap 後,一方面會往該屬性的 depsMap 中添加當前 activeEffect,即收集訂閱者。另外一方面,將該屬性的 depsMap 添加到 activeEffectdeps 數組中,即訂閱主題。從而,造成整個依賴收集過程。

4、派發更新

set

分析完依賴收集的過程,那麼派發更新的整個過程的分析也將會水到渠成。首先,對應派發更新,是指當某個主題發生變化時,在咱們這個 case 是當 count 發生變化時,此時會觸發 dataset(),即 targetdatakeycount

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;
    };

能夠看到,oldValue0,而咱們的 shallow 此時爲 falsevalue 爲 1。那麼,咱們看一下 toRaw() 函數的邏輯:

function toRaw(observed) {
    return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed;
}

toRaw() 中有兩個 WeakMap 類型的變量 reactiveToRawreadonlyRaw。前者是在初始化 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() 函數,首先獲取當前 targetMapdata 對應的主題對象的 depsMap,而這個 depsMap 即咱們在依賴收集時在 track 中定義的。

而後,初始化兩個 Set 集合 effectscomputedRunners ,用於記錄普通屬性或計算屬性的 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() 記錄該 jobeffect() 觸發的次數。若是超過 100 次會拋出錯誤。
  • 而後調用 callWithErrorHandling(),執行 jobeffect(),而咱們都知道的是這個 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(),最後更新組件。

寫做不易,若是你以爲有收穫的話,能夠帥氣三連擊!!!
相關文章
相關標籤/搜索