【原始碼&庫】Vue3 的響應式核心 reactive 和 effect 實現原理以及原始碼分析

田八發表於2023-03-07

Vue的響應式系統很讓人著迷,Vue2使用的是Object.definePropertyVue3使用的是Proxy,這個是大家都知道的技術點;

但是知道了這些個技術點就能寫出一個響應式系統嗎?答案是肯定是NOVue的響應式系統是一個非常複雜的系統,技術只是實現的手段,今天我們就來看看背後實現的思想。

本章內容有點多,如果不能耐著性子看的話,建議先看看我線上實現的小 demo 去理解核心思想,不明白再來在文章中尋找答案:https://codesandbox.io/s/magical-knuth-mjyh7j?file=/src/main.js

reactive 和 effect

Vue3的響應式系統透過官網的API可以看到有很多,例如refcomputedreactivereadonlywatchEffectwatch等等,這些都是Vue3的響應式系統的一部分;

reactive

reactive根據官網的介紹,有如下特點:

  1. 接收一個普通物件,返回一個響應式的代理物件;
  2. 響應式的物件是深層的,會影響物件內部所有巢狀的屬性;
  3. 會自動對ref物件進行解包;
  4. 對於陣列、物件、MapSet等原生型別中的元素,如果是ref物件不會自動解包;
  5. 返回的物件會透過Proxy進行包裝,所以不等於原始物件;

上面的這些特點都是可以在官網中有介紹,如果我說的不是很好理解建議去官網看看,官網對這些特點都有詳細的介紹,並且還有示例程式碼。

對於reactive的作用其實使用Vue3的同學都知道是幹嘛的,就不多說了。

effect

effect在官網上是沒有提到這個API的,但是在原始碼中是有的,並且我們也是可以直接使用,如下程式碼所示:

import { reactive, effect } from "vue";

const data = reactive({
  foo: 1,
  bar: 2
});

effect(() => {
  console.log(data.foo);
});

data.foo = 10;

通常情況下我們是不會直接使用effect的,因為effect是一個底層的API,在我們使用Vue3的時候Vue預設會幫我們呼叫effect,所以我們的關注點通常都是在reactive上。

但是reactive需要和effect配合使用才會有響應式的效果,所以我們需要了解一下effect的作用。

effect直接翻譯為作用,意思是使其發生作用,這個使其就是我們傳入的函式,所以effect的作用就是讓我們傳入的函式發生作用,也就是執行這個函式。

但是effect是怎麼知道我們傳入的函式需要執行呢?這些答案都在原始碼中,現在來進入正式的原始碼閱讀環節。

原始碼

Vue的響應式系統的原始碼在packages/reactivity目錄下,Vue3將其單獨抽離出來為一個獨立的系統,我們可以看看這個工程的README檔案;

根據README檔案中的介紹,響應系統被內聯到面向使用者的生產和開發構建的包中,但是也可以單獨使用。

如果你想單獨使用的話,建議不要和Vue混合使用,因為獨立使用的話,和Vue的響應式系統內部的資料並不互通,這樣就會有兩個響應式系統發揮作用,這樣可能會有產生一些不可預知的問題。

響應式系統出了對ArrayMapWeakMapSetWeakSet這些原生型別進行了響應式處理,對其他的原生型別,例如DateRegExpError等等,都沒有進行響應式處理。

reactive

reactive的原始碼在packages/reactivity/src/reactive.ts檔案中,還是老樣子,我們不看原始的ts程式碼,直接看編譯後的js程式碼,這樣更容易理解。

function reactive(target) {
    // 如果對只讀的代理物件進行再次代理,那麼應該返回原始的只讀代理物件
    if (isReadonly(target)) {
        return target;
    }
    
    // 透過 createReactiveObject 方法建立響應式物件
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

reactive的原始碼很簡單,就是呼叫了createReactiveObject方法,這個方法是一個工廠方法,用來建立響應式物件的,我們來看看這個方法的原始碼。

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    // 如果 target 不是物件,那麼直接返回 target
    if (!isObject(target)) {
        {
            console.warn(`value cannot be made reactive: ${String(target)}`);
        }
        return target;
    }
    
    // 如果 target 已經是一個代理物件了,那麼直接返回 target
    // 異常:如果對一個響應式物件呼叫 readonly() 方法
    if (target["__v_raw" /* ReactiveFlags.RAW */] &&
        !(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) {
        return target;
    }
    
    // 如果 target 已經有對應的代理物件了,那麼直接返回代理物件
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
        return existingProxy;
    }
    
    // 對於不能被觀察的型別,直接返回 target
    const targetType = getTargetType(target);
    if (targetType === 0 /* TargetType.INVALID */) {
        return target;
    }
    
    // 建立一個響應式物件
    const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
    
    // 將 target 和 proxy 儲存到 proxyMap 中
    proxyMap.set(target, proxy);
    
    // 返回 proxy
    return proxy;
}

createReactiveObject方法的原始碼也很簡單,最開始的一些程式碼都是對需要代理的target進行一些判斷,判斷的邊界都是target不是物件的情況和target已經是一個代理物件的情況;

其中的核心的程式碼主要是最後七行程式碼:

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    // 對於不能被觀察的型別,直接返回 target
    const targetType = getTargetType(target);
    if (targetType === 0 /* TargetType.INVALID */) {
        return target;
    }
    
    // 建立一個響應式物件
    const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
    
    // 將 target 和 proxy 儲存到 proxyMap 中
    proxyMap.set(target, proxy);
    
    // 返回 proxy
    return proxy;
}

這裡有一個targetType的判斷,那麼這個targetType是什麼呢?我們來看看getTargetType方法的原始碼:


// 獲取原始資料型別
const toRawType = (value) => {
    // extract "RawType" from strings like "[object RawType]"
    return toTypeString(value).slice(8, -1);
};

// 獲取資料型別
function targetTypeMap(rawType) {
    switch (rawType) {
        case 'Object':
        case 'Array':
            return 1 /* TargetType.COMMON */;
        case 'Map':
        case 'Set':
        case 'WeakMap':
        case 'WeakSet':
            return 2 /* TargetType.COLLECTION */;
        default:
            return 0 /* TargetType.INVALID */;
    }
}

// 獲取 target 的型別
function getTargetType(value) {
    return value["__v_skip" /* ReactiveFlags.SKIP */] || !Object.isExtensible(value)
        ? 0 /* TargetType.INVALID */
        : targetTypeMap(toRawType(value));
}

這裡主要看的是Vue寫的程式碼註釋,這裡的註釋是Vuets原始碼中的列舉型別,最後返回的值列舉型別的值:

const enum TargetType {
    // 無效的資料型別,對應的值是 0,表示 Vue 不會對這種型別的資料進行響應式處理
    INVALID = 0,
    // 普通的資料型別,對應的值是 1,表示 Vue 會對這種型別的資料進行響應式處理
    COMMON = 1,
    // 集合型別,對應的值是 2,表示 Vue 會對這種型別的資料進行響應式處理
    COLLECTION = 2
}

export const enum ReactiveFlags {
    // 用於標識一個物件是否不可被轉為代理物件,對應的值是 __v_skip
    SKIP = '__v_skip',
    // 用於標識一個物件是否是響應式的代理,對應的值是 __v_isReactive
    IS_REACTIVE = '__v_isReactive',
    // 用於標識一個物件是否是隻讀的代理,對應的值是 __v_isReadonly
    IS_READONLY = '__v_isReadonly',
    // 用於標識一個物件是否是淺層代理,對應的值是 __v_isShallow
    IS_SHALLOW = '__v_isShallow',
    // 用於儲存原始物件的 key,對應的值是 __v_raw
    RAW = '__v_raw'
}

這裡的列舉值以及含義都列出來了,然後結合原始碼,我們就可以更清晰的理解每段的程式碼的含義了。

collectionHandlers 和 baseHandlers

其實代理根據這幾年的推廣,早就不是什麼新鮮事物了,createReactiveObject方法最後返回的就是一個代理物件;

關鍵點就在於這個代理物件的handler,而這個handler就是collectionHandlersbaseHandlers這兩個物件;

原始碼中透過targetType來判斷使用哪個handlertargetType2的時候使用collectionHandlers,否則使用baseHandlers

其實這個targetType根據列舉值也就只有3個值,最後走向代理的也就只有兩種情況:

  • targetType1的時候,這個時候target是一個普通的物件或者陣列,這個時候使用baseHandlers
  • targetType2的時候,這個時候target是一個集合型別,這個時候使用collectionHandlers

而這兩個handler的是透過外部傳入的,也就是createReactiveObject方法的第三個和第四個引數,而傳入這兩個引數的地方就是reactive方法:

function reactive(target) {
    // ...
    
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

可以看到的是mutableHandlersmutableCollectionHandlers分別對應baseHandlerscollectionHandlers

而這兩個handler的定義在reactivity/src/baseHandlers.tsreactivity/src/collectionHandlers.ts中;

感興趣的可以去翻看一下這兩個檔案的原始碼,這裡還是貼出打包之後的程式碼,先從baseHandlers開始;

baseHandlers

注意這裡的baseHandlers指向的是mutableHandlersmutableHandlersbaseHandlers的一個export

const mutableHandlers = {
    get: get$1,
    set: set$1,
    deleteProperty,
    has: has$1,
    ownKeys
};

這裡分別定義了getsetdeletePropertyhasownKeys這幾個方法攔截器,簡單介紹一下作用:

  • get:攔截物件的getter操作,比如obj.name
  • set:攔截物件的setter操作,比如obj.name = '田八'
  • deleteProperty:攔截delete操作,比如delete obj.name
  • has:攔截in操作,比如'name' in obj
  • ownKeys:攔截Object.getOwnPropertyNamesObject.getOwnPropertySymbolsObject.keys等操作;

更具體的可以看看MDN的介紹;

再來看看這些個攔截器的具體實現。

get
const get$1 = /*#__PURE__*/ createGetter();
function createGetter(isReadonly = false, shallow = false) {
    // 閉包返回 get 攔截器方法
    return function get(target, key, receiver) {
        // 如果訪問的是 __v_isReactive 屬性,那麼返回 isReadonly 的取反值
        if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) {
            return !isReadonly;
        }
        
        // 如果訪問的是 __v_isReadonly 屬性,那麼返回 isReadonly 的值
        else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) {
            return isReadonly;
        }
        
        // 如果訪問的是 __v_isShallow 屬性,那麼返回 shallow 的值
        else if (key === "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */) {
            return shallow;
        }
        
        // 如果訪問的是 __v_raw 屬性,並且有一堆條件滿足,那麼返回 target
        else if (key === "__v_raw" /* ReactiveFlags.RAW */ &&
            receiver ===
            (isReadonly
                ? shallow
                    ? shallowReadonlyMap
                    : readonlyMap
                : shallow
                    ? shallowReactiveMap
                    : reactiveMap).get(target)) {
            return target;
        }
        
        // target 是否是陣列
        const targetIsArray = isArray(target);
        
        // 如果不是隻讀的
        if (!isReadonly) {
            // 如果是陣列,並且訪問的是陣列的一些方法,那麼返回對應的方法
            if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
                return Reflect.get(arrayInstrumentations, key, receiver);
            }
            
            // 如果訪問的是 hasOwnProperty 方法,那麼返回 hasOwnProperty 方法
            if (key === 'hasOwnProperty') {
                return hasOwnProperty;
            }
        }
        
        // 獲取 target 的 key 屬性值
        const res = Reflect.get(target, key, receiver);
        
        // 如果是內建的 Symbol,或者是不可追蹤的 key,那麼直接返回 res
        if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
            return res;
        }
        
        // 如果不是隻讀的,那麼進行依賴收集
        if (!isReadonly) {
            track(target, "get" /* TrackOpTypes.GET */, key);
        }
        
        // 如果是淺的,那麼直接返回 res
        if (shallow) {
            return res;
        }
        
        // 如果 res 是 ref,對返回的值進行解包
        if (isRef(res)) {
            // 對於陣列和整數型別的 key,不進行解包
            return targetIsArray && isIntegerKey(key) ? res : res.value;
        }
        
        // 如果 res 是物件,遞迴代理
        if (isObject(res)) {
            // 將返回的值也轉換為代理。我們在這裡進行 isObject 檢查,以避免無效的值警告。
            // 還需要延遲訪問 readonly 和 reactive,以避免迴圈依賴。
            return isReadonly ? readonly(res) : reactive(res);
        }
        
        // 返回 res
        return res;
    };
}

稍微有點複雜,但是也不難理解,我來拆解一下:

function get(target, key, receiver) {
    // 如果訪問的是 __v_isReactive 屬性,那麼返回 isReadonly 的取反值
    if (key === "__v_isReactive" /* ReactiveFlags.IS_REACTIVE */) {
        return !isReadonly;
    }
    
    // 如果訪問的是 __v_isReadonly 屬性,那麼返回 isReadonly 的值
    else if (key === "__v_isReadonly" /* ReactiveFlags.IS_READONLY */) {
        return isReadonly;
    }
    
    // 如果訪問的是 __v_isShallow 屬性,那麼返回 shallow 的值
    else if (key === "__v_isShallow" /* ReactiveFlags.IS_SHALLOW */) {
        return shallow;
    }
    
    // 如果訪問的是 __v_raw 屬性,並且有一堆條件滿足,那麼返回 target
    else if (key === "__v_raw" /* ReactiveFlags.RAW */ &&
        receiver ===
        (isReadonly
            ? shallow
                ? shallowReadonlyMap
                : readonlyMap
            : shallow
                ? shallowReactiveMap
                : reactiveMap).get(target)) {
        return target;
    }
   
    // ...
};

這一段程式碼是為了處理一些特殊的屬性,這些都是Vue內部定義好的,就是上面提到過的列舉值,用於判斷是否是reactivereadonlyshallow等等。

這一段程式碼對於我們理解原始碼並不重要,重要的是下面一段:

function get(target, key, receiver) {
    // ...
    
    // target 是否是陣列
    const targetIsArray = isArray(target);
    
    // 如果不是隻讀的
    if (!isReadonly) {
        // 如果是陣列,並且訪問的是陣列的一些方法,那麼返回對應的方法
        if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
            return Reflect.get(arrayInstrumentations, key, receiver);
        }
        
        // 如果訪問的是 hasOwnProperty 方法,那麼返回 hasOwnProperty 方法
        if (key === 'hasOwnProperty') {
            return hasOwnProperty;
        }
    }
    
    // 獲取 target 的 key 屬性值
    const res = Reflect.get(target, key, receiver);
    
    // 如果是內建的 Symbol,或者是不可追蹤的 key,那麼直接返回 res
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
        return res;
    }
    
    // 如果不是隻讀的,那麼進行依賴收集
    if (!isReadonly) {
        track(target, "get" /* TrackOpTypes.GET */, key);
    }
    
    // 如果是淺的,那麼直接返回 res
    if (shallow) {
        return res;
    }
    
    // 如果 res 是 ref,對返回的值進行解包
    if (isRef(res)) {
        // 對於陣列和整數型別的 key,不進行解包
        return targetIsArray && isIntegerKey(key) ? res : res.value;
    }
    
    // 如果 res 是物件,遞迴代理
    if (isObject(res)) {
        // 將返回的值也轉換為代理。我們在這裡進行 isObject 檢查,以避免無效的值警告。
        // 還需要延遲訪問 readonly 和 reactive,以避免迴圈依賴。
        return isReadonly ? readonly(res) : reactive(res);
    }
    
    // 返回 res
    return res;
};

這一段還是太多了,但是其實每段程式碼都是為了完成一個獨立的需求,我們再來拆解一下:

  • 對陣列的方法訪問處理
function get(target, key, receiver) {
    // ...
    
    // target 是否是陣列
    const targetIsArray = isArray(target);
    
    // 如果不是隻讀的
    if (!isReadonly) {
        // 如果是陣列,並且訪問的是陣列的一些方法,那麼返回對應的方法
        if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
            return Reflect.get(arrayInstrumentations, key, receiver);
        }
        
        // 如果訪問的是 hasOwnProperty 方法,那麼返回 hasOwnProperty 方法
        if (key === 'hasOwnProperty') {
            return hasOwnProperty;
        }
    }
    
    // ...
};

這一段程式碼是為了處理陣列的一些方法,比如pushpop等等,如果我們在呼叫這些方法的時候,就會進入這一段程式碼,然後返回對應的方法,例如:

const arr = reactive([1, 2, 3]);

arr.push(4);

這些方法都在arrayInstrumentations中,這次不做重點分析,後面會專門講解。

  • 獲取返回值,返回值的特別對待
function get(target, key, receiver) {
    // ...
    
    // 獲取 target 的 key 屬性值
    const res = Reflect.get(target, key, receiver);
    
    // ...
};

走到這裡,就需要獲取targetkey屬性值了,這裡使用了Reflect.get

這個方法是ES6中新增的,用於訪問物件的屬性,和target[key]是等價的,但是Reflect.get可以傳入receiver,這個引數是用來繫結this的;

這是為瞭解決Proxythis指向問題,這裡不做過多的解釋,後面會專門講解,Reflect不瞭解的看:MDN Reflect

  • 特殊屬性的不進行依賴收集
function get(target, key, receiver) {
    // ...
    
    // 如果是內建的 Symbol,或者是不可追蹤的 key,那麼直接返回 res
    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
        return res;
    }
    
    // ...
};

這一步是為了過濾一些特殊的屬性,例如原生的Symbol型別的屬性,如:Symbol.iteratorSymbol.toStringTag等等,這些屬性不需要進行依賴收集,因為它們是內建的,不會改變;

還有一些不可追蹤的屬性,如:__proto____v_isRef__isVue這些屬性也不需要進行依賴收集;

  • 進行依賴收集
function get(target, key, receiver) {
    // ...
    
    // 如果不是隻讀的,那麼進行依賴收集
    if (!isReadonly) {
        track(target, "get" /* TrackOpTypes.GET */, key);
    }
    
    // ...
};

這一步是為了進行依賴收集,這裡呼叫了track方法,這個方法在effect中會用到,稍後會講解;

  • 淺的不進行遞迴代理
function get(target, key, receiver) {
    // ...
    
    // 如果是淺的,那麼直接返回 res
    if (shallow) {
        return res;
    }
    
    // ...
};

這一步是為了處理shallow的情況,如果是shallow的,那麼就不需要進行遞迴代理了,直接返回res即可;

  • 對返回值進行解包
function get(target, key, receiver) {
    // ...
    
    // 如果 res 是 ref,對返回的值進行解包
    if (isRef(res)) {
        // 對於陣列和整數型別的 key,不進行解包
        return targetIsArray && isIntegerKey(key) ? res : res.value;
    }
    
    // ...
};

這一步是為了處理ref的情況,如果resref,那麼就對res進行解包,這裡有一個判斷,如果是陣列,並且key是整數型別,那麼就不進行解包;

  • 對返回值進行代理
function get(target, key, receiver) {
    // ...
    
    // 如果 res 是物件,那麼對返回的值進行代理
    if (isObject(res)) {
        return isReadonly ? readonly(res) : reactive(res);
    }
    
    // ...
};

如果是物件,那麼就對res進行代理,這裡有一個判斷,如果是readonly的,那麼就使用readonly方法進行代理,否則就使用reactive方法進行代理;

最後就是返回res了,這裡就是Vue3get方法的全部內容了,其實拆分下來就容易理解多了,下面我們來看看Vue3set方法;

set
const set$1 = /*#__PURE__*/ createSetter();

function createSetter(shallow = false) {
    // 閉包返回一個 set 方法
    return function set(target, key, value, receiver) {
        // 獲取舊值
        let oldValue = target[key];

        // 如果舊值是隻讀的,並且是 ref,並且新值不是 ref,那麼直接返回 false,代表設定失敗
        if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
            return false;
        }

        // 如果不是淺的
        if (!shallow) {

            // 如果新值不是淺的,並且不是隻讀的
            if (!isShallow(value) && !isReadonly(value)) {
                // 獲取舊值的原始值
                oldValue = toRaw(oldValue);
                // 獲取新值的原始值
                value = toRaw(value);
            }

            // 如果目標物件不是陣列,並且舊值是 ref,並且新值不是 ref,那麼設定舊值的 value 為新值,並且返回 true,代表設定成功
            // ref 的值是在 value 屬性上的,這裡判斷了舊值的代理型別,所以設定到了舊值的 value 上
            if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
                oldValue.value = value;
                return true;
            }
        }

        // 如果是陣列,並且 key 是整數型別
        const hadKey = isArray(target) && isIntegerKey(key)
            // 如果 key 小於陣列的長度,那麼就是有這個 key
            ? Number(key) < target.length
            // 如果不是陣列,那麼就是普通物件,直接判斷是否有這個 key
            : hasOwn(target, key);

        // 透過 Reflect.set 設定值
        const result = Reflect.set(target, key, value, receiver);

        // 如果目標物件是原始資料的原型鏈中的某個元素,則不會觸發依賴收集
        if (target === toRaw(receiver)) {
            // 如果沒有這個 key,那麼就是新增了一個屬性,觸發 add 事件
            if (!hadKey) {
                trigger(target, "add" /* TriggerOpTypes.ADD */, key, value);
            }

            // 如果有這個 key,那麼就是修改了一個屬性,觸發 set 事件
            else if (hasChanged(value, oldValue)) {
                trigger(target, "set" /* TriggerOpTypes.SET */, key, value, oldValue);
            }
        }

        // 返回結果,這個結果為 boolean 型別,代表是否設定成功
        // 只是代理相關,,和業務無關,必須要返回是否設定成功的結果
        return result;
    };
}

set方法的實現其實整體要比get方法的實現要複雜一些,雖然程式碼比get要少一些,不過整體梳理下來,大體分為下面幾個步驟:

  • 獲取舊值
function set(target, key, value, receiver) {
    // 獲取舊值
    let oldValue = target[key];
    
    // ...
};

這裡的舊值就是target[key]的值,舊值在Vue3中有很多用處,會貫穿整個流程,這裡先不展開講,後面會講到;

  • 判斷舊值是否是隻讀的
function set(target, key, value, receiver) {
    // ...
    
    // 如果舊值是隻讀的,並且是 ref,並且新值不是 ref,那麼直接返回 false,代表設定失敗
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
        return false;
    }
    
    // ...
};

只讀的ref是不能被修改的,所以這裡就直接返回false了,代表設定失敗;

但是這裡需要有很多的條件,首先舊值必須是隻讀的,其次舊值必須是ref,最後新值不能是ref,如下面的例子:

    const refObj = ref({
    a: 1,
    b: 2
});
const readonlyObj = readonly(refObj);

const obj = reactive({
    readonlyObj
})

obj.readonlyObj = 10;
console.log(obj.readonlyObj); // 設定失敗

obj.readonlyObj = ref(10);
console.log(obj.readonlyObj); // 設定成功

很奇怪的判定,個人的知識儲備量還不夠,沒想明白為什麼要有這麼樣一個的判定才會設定失敗。

  • 判斷是否是淺的
function set(target, key, value, receiver) {
    // ...
    
    // 如果不是淺的
    if (!shallow) {
        // ...
    }
    
    // ...
};

在判斷是否不是淺層響應的時候,這個引數是透過閉包儲存下來的,不是淺層響應的時候,這個內部會做兩件事情:

  1. 獲取舊值的原始值和新值的原始值
function set(target, key, value, receiver) {
    // ...
    
    // 如果不是淺的
    if (!shallow) {
        // 如果新值不是淺的,並且不是隻讀的
        if (!isShallow(value) && !isReadonly(value)) {
            // 獲取舊值的原始值
            oldValue = toRaw(oldValue);
            // 獲取新值的原始值
            value = toRaw(value);
        }
    }
    
    // ...
};

這裡需要先判斷新值是否不是淺層響應的,並且不是隻讀的,如果是的話,那麼就不需要獲取原始值了,因為這個時候新值就是原始值了;

這裡因為如果新值是淺層響應的,那就說明這個響應式物件的元素只有一層響應式,只會關心當前物件的響應式,當前物件的元素是否是響應式的就不關心了,所以不用獲取原始值,直接覆蓋原則就可以了;

deleteProperty
function deleteProperty(target, key) {
    // 當前物件是否有這個 key
    const hadKey = hasOwn(target, key);
    
    // 舊值
    const oldValue = target[key];
    
    // 透過 Reflect.deleteProperty 刪除屬性
    const result = Reflect.deleteProperty(target, key);
    
    // 如果刪除成功,並且當前物件有這個 key,那麼就觸發 delete 事件
    if (result && hadKey) {
        trigger(target, "delete" /* TriggerOpTypes.DELETE */, key, undefined, oldValue);
    }
    
    // 返回結果,這個結果為 boolean 型別,代表是否刪除成功
    return result;
}

deleteProperty方法的實現對比getset方法的實現都要簡單很多,也沒有什麼特別的地方,就是透過Reflect.deleteProperty刪除屬性,然後透過trigger觸發delete事件,最後返回刪除是否成功的結果;

has
function has$1(target, key) {
    // 透過 Reflect.has 判斷當前物件是否有這個 key
    const result = Reflect.has(target, key);
    
    // 如果當前物件不是 Symbol 型別,或者當前物件不是內建的 Symbol 型別,那麼就觸發 has 事件
    if (!isSymbol(key) || !builtInSymbols.has(key)) {
        track(target, "has" /* TrackOpTypes.HAS */, key);
    }
    
    // 返回結果,這個結果為 boolean 型別,代表當前物件是否有這個 key
    return result;
}

has方法的實現也是比較簡單的,就是透過Reflect.has判斷當前物件是否有這個 key,然後透過track觸發has事件,最後返回是否有這個 key 的結果;

ownKeys
function ownKeys(target) {
    // 直接觸發 iterate 事件
    track(target, "iterate" /* TrackOpTypes.ITERATE */, isArray(target) ? 'length' : ITERATE_KEY);
    
    // 透過 Reflect.ownKeys 獲取當前物件的所有 key
    return Reflect.ownKeys(target);
}

ownKeys方法的實現也是比較簡單的,直接觸發iterate事件,然後透過Reflect.ownKeys獲取當前物件的所有 key,最後返回這些 key;

注意點在於對陣列的特殊處理,如果當前物件是陣列的話,那麼就會觸發lengthiterate事件,如果不是陣列的話,那麼就會觸發ITERATE_KEYiterate事件;

這一塊的區別都是在track方法中才會有體現,這個就是響應式的核心思路,後面會詳細講解;

effect

上面講完了reactive方法,接下來就是effect方法,effect方法的作用是建立一個副作用函式,這個函式會在依賴的資料發生變化的時候執行;

依賴收集和觸發更新的過程先不要著急,等講完effect方法之後,再來分析這個過程,先看看effect方法的實現:

function effect(fn, options) {
    // 如果 fn 物件上有 effect 屬性
    if (fn.effect) {
        // 那麼就將 fn 替換為 fn.effect.fn
        fn = fn.effect.fn;
    }
    
    // 建立一個響應式副作用函式
    const _effect = new ReactiveEffect(fn);
    
    // 如果有配置項
    if (options) {
        // 將配置項合併到響應式副作用函式上
        extend(_effect, options);
        
        // 如果配置項中有 scope 屬性(該屬性的作用是指定副作用函式的作用域)
        if (options.scope)
            // 那麼就將 scope 屬性記錄到響應式副作用函式上(類似一個作用域鏈)
            recordEffectScope(_effect, options.scope);
    }
    
    // 如果沒有配置項,或者配置項中沒有 lazy 屬性,或者配置項中的 lazy 屬性為 false
    if (!options || !options.lazy) {
        // 那麼就執行響應式副作用函式
        _effect.run();
    }
    
    // 將 _effect.run 的 this 指向 _effect
    const runner = _effect.run.bind(_effect);
    
    // 將響應式副作用函式賦值給 runner.effect
    runner.effect = _effect;
    
    // 返回 runner
    return runner;
}

其實這裡的原始碼一下並不能看明白具體想要幹嘛,而且內部的呼叫,或者說資料的指向也比較複雜;

但是梳理下來,這裡的關鍵點有兩個部分:

  1. 建立一個響應式副作用函式const _effect = new ReactiveEffect(fn)
  2. 返回一個runner函式,可以透過這個函式來執行響應式副作用函式;

ReactiveEffect

先來分析下ReactiveEffect這個類,這個類的作用是建立一個響應式副作用函式,這個函式會在依賴的資料發生變化的時候執行;

class ReactiveEffect {
    constructor(fn, scheduler = null, scope) {
        // 副作用函式
        this.fn = fn;
        // 排程器,用於控制副作用函式何時執行
        this.scheduler = scheduler;
        // 標誌位,用於標識當前 ReactiveEffect 物件是否處於活動狀態
        this.active = true;
        // 響應式依賴項的集合
        this.deps = [];
        // 父級作用域
        this.parent = undefined;
        
        // 記錄當前 ReactiveEffect 物件的作用域
        recordEffectScope(this, scope);
    }
    run() {
        // ...
    }
    stop() {
        // ...
    }
}

ReactiveEffect這個類的實現主要體現在兩個方法上,一個是run方法,一個是stop方法;

其他的屬性都是用來記錄一些資料的,比如fn屬性就是用來記錄副作用函式的,scheduler屬性就是用來記錄排程器的,active屬性就是用來記錄當前ReactiveEffect物件是否處於活動狀態的;

這些屬性的具體作用將在下面的分析中講解,先來看看run方法的實現;

run

function run() {
    // 如果當前 ReactiveEffect 物件不處於活動狀態,直接返回 fn 的執行結果
    if (!this.active) {
        return this.fn();
    }
    
    // 尋找當前 ReactiveEffect 物件的最頂層的父級作用域
    let parent = activeEffect;
    let lastShouldTrack = shouldTrack;
    while (parent) {
        if (parent === this) {
            return;
        }
        parent = parent.parent;
    }
    
    try {
        // 記錄父級作用域為當前活動的 ReactiveEffect 物件
        this.parent = activeEffect;
        
        // 將當前活動的 ReactiveEffect 物件設定為 “自己”
        activeEffect = this;
        
        // 將 shouldTrack 設定為 true (表示是否需要收集依賴)
        shouldTrack = true;
        
        // effectTrackDepth 用於標識當前的 effect 呼叫棧的深度,執行一次 effect 就會將 effectTrackDepth 加 1
        trackOpBit = 1 << ++effectTrackDepth;
        
        // 這裡是用於控制 "effect呼叫棧的深度" 在一個閾值之內
        if (effectTrackDepth <= maxMarkerBits) {
            // 初始依賴追蹤標記
            initDepMarkers(this);
        }
        else {
            // 清除所有的依賴追蹤標記
            cleanupEffect(this);
        }
        
        // 執行副作用函式,並返回執行結果
        return this.fn();
    }
    finally {
        // 如果 effect呼叫棧的深度 沒有超過閾值
        if (effectTrackDepth <= maxMarkerBits) {
            // 確定最終的依賴追蹤標記
            finalizeDepMarkers(this);
        }
        
        // 執行完畢會將 effectTrackDepth 減 1
        trackOpBit = 1 << --effectTrackDepth;
        
        // 執行完畢,將當前活動的 ReactiveEffect 物件設定為 “父級作用域”
        activeEffect = this.parent;
        
        // 將 shouldTrack 設定為上一個值
        shouldTrack = lastShouldTrack;
        
        // 將父級作用域設定為 undefined
        this.parent = undefined;
        
        // 延時停止,這個標誌是在 stop 方法中設定的
        if (this.deferStop) {
            this.stop();
        }
    }
}

整體梳理下來,run方法的作用就是執行副作用函式,並且在執行副作用函式的過程中,會收集依賴;

整體的流程還是非常複雜的,但是這裡的核心思想是各種標識位的設定,以及在執行副作用函式的過程中,會收集依賴;

這裡的流程沒必要一下就全都瞭解,現在只需要記住下面這樣的流程就可以了:

stop

function stop() {
    // 如果當前 活動的 ReactiveEffect 物件是 “自己”
    // 延遲停止,需要執行完當前的副作用函式之後再停止
    if (activeEffect === this) {
        // 在 run 方法中會判斷 deferStop 的值,如果為 true,就會執行 stop 方法
        this.deferStop = true;
    }
    
    // 如果當前 ReactiveEffect 物件處於活動狀態
    else if (this.active) {
        // 清除所有的依賴追蹤標記
        cleanupEffect(this);
        
        // 如果有 onStop 回撥函式,就執行
        if (this.onStop) {
            this.onStop();
        }
        
        // 將 active 設定為 false
        this.active = false;
    }
}

stop方法的作用就是停止當前的ReactiveEffect物件,停止之後,就不會再收集依賴了;

這裡的activeEffectthis並不是每次都相等的,因為activeEffect會跟著呼叫棧的深度而變化,而this則是固定的;

this.active標識的自身是否處在活動狀態,因為巢狀的ReactiveEffect物件,activeEffect並不一定指向自己,而this.active則是自身的狀態;

依賴收集

講了reactiveeffect之後,我們就可以來講講依賴收集了;

上面講了這麼多,他們兩個好像還沒有聯絡起來,好像是相互獨立的,而他們的聯絡的紐帶就是activeEffect

常聽人說響應式系統在getter中收集依賴,在setter中觸發依賴,現在回頭看看getter是怎麼收集依賴的;

track

現在回憶一下getter的實現,裡面有這樣的一段程式碼:

function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        // ...
        
        // 如果不是隻讀的,就會收集依賴
        if (!isReadonly) {
            track(target, "get" /* TrackOpTypes.GET */, key);
        }
        
        // ...
        
        return res;
    };
}

track方法的作用就是收集依賴,它的實現如下:

const targetMap = new WeakMap();

/**
 * 收集依賴
 * @param target 指向的物件
 * @param type 操作型別
 * @param key 指向物件的 key
 */
function track(target, type, key) {
    // 如果 shouldTrack 為 false,並且 activeEffect 沒有值的話,就不會收集依賴
    if (shouldTrack && activeEffect) {
        
        // 如果 targetMap 中沒有 target,就會建立一個 Map
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
        }
        
        // 如果 depsMap 中沒有 key,就會建立一個 Set
        let dep = depsMap.get(key);
        if (!dep) {
            depsMap.set(key, (dep = createDep()));
        }
        
        // 將當前的 ReactiveEffect 物件新增到 dep 中
        const eventInfo = {
            effect: activeEffect,
            target,
            type,
            key
        };
        
        // 如果 dep 中沒有當前的 ReactiveEffect 物件,就會新增進去
        trackEffects(dep, eventInfo);
    }
}

在這裡我們發現了兩個老熟人,一個是shouldTrack,一個是activeEffect,這兩個變數都是在effect方法中出現過的;

shouldTrack在上面也講過,它的作用就是控制是否收集依賴,暫時不用深入;

activeEffect就是我們剛剛講的ReactiveEffect物件,它指向的就是當前正在執行的副作用函式;

track方法的作用就是收集依賴,它的實現非常簡單,就是在targetMap中記錄下targetkey

targetMap是一個WeakMap,它的鍵是target,值是一個Map,這個Map的鍵是key,值是一個Set

這意味著,如果我們在操作targetkey時,就會收集依賴,這個時候,targetkey就會被記錄到targetMap中,用程式碼表示就是:

const obj = {
    a: 1,
    b: 2
};

const targetMap = new WeakMap();

// 我在操作 obj.a 的時候,就會收集依賴
obj.a;

// 這個時候,targetMap 中就會記錄下 obj 和 a
let depsMap = targetMap.get(obj);
if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
}

// createDep 實現很簡單,就不在講解的程式碼裡面單獨寫出來了,具體就是一個 Set,多了兩個屬性,w 和 n
const createDep = (effects) => {
    const dep = new Set(effects);
    dep.w = 0; // 指向的是 watcher 物件的唯一標識
    dep.n = 0; // 指向的是不同的 dep 的唯一標識
    return dep;
};


let dep = depsMap.get("a");
if (!dep) {
    depsMap.set("a", (dep = createDep()));
}

// dep 就是一個 Set,裡面存放的就是當前的 ReactiveEffect 物件
dep.add(activeEffect);

上面就是一個收集依賴的過程,我們可以看到,targetMap中記錄的是targetkey,而dep中記錄的是ReactiveEffect物件;

trigger

現在我們來看看trigger方法,它的作用就是觸發依賴,它的實現如下:

/**
 * 觸發依賴
 * @param target 指向的物件
 * @param type 操作型別
 * @param key 指向物件的 key
 * @param newValue 新值
 * @param oldValue 舊值
 * @param oldTarget 舊的 target
 */
function trigger(target, type, key, newValue, oldValue, oldTarget) {
    // 獲取 targetMap 中的 depsMap
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        // never been tracked
        return;
    }
    
    // 建立一個陣列,用來存放需要執行的 ReactiveEffect 物件
    let deps = [];
    
    // 如果 type 為 clear,就會將 depsMap 中的所有 ReactiveEffect 物件都新增到 deps 中
    if (type === "clear" /* TriggerOpTypes.CLEAR */) {
        // 執行所有的 副作用函式
        deps = [...depsMap.values()];
    }
    
    // 如果 key 為 length ,並且 target 是一個陣列
    else if (key === 'length' && isArray(target)) {
        // 修改陣列的長度,會導致陣列的索引發生變化
        // 但是隻有兩種情況,一種是陣列的長度變大,一種是陣列的長度變小
        // 如果陣列的長度變大,那麼執行所有的副作用函式就可以了
        // 如果陣列的長度變小,那麼就需要執行索引大於等於新陣列長度的副作用函式
        const newLength = Number(newValue);
        depsMap.forEach((dep, key) => {
            if (key === 'length' || key >= newLength) {
                deps.push(dep);
            }
        });
    }
    
    // 其他情況
    else {
        // key 不是 undefined,就會將 depsMap 中 key 對應的 ReactiveEffect 物件新增到 deps 中
        // void 0 就是 undefined
        if (key !== void 0) {
            deps.push(depsMap.get(key));
        }
        
        
        // 執行 add、delete、set 操作時,就會觸發的依賴變更
        switch (type) {
            // 如果 type 為 add,就會觸發的依賴變更
            case "add" /* TriggerOpTypes.ADD */:
                // 如果 target 不是陣列,就會觸發迭代器
                if (!isArray(target)) {
                    // ITERATE_KEY 再上面介紹過,用來標識迭代屬性
                    // 例如:for...in、for...of,這個時候依賴會收集到 ITERATE_KEY 上
                    // 而不是收集到具體的 key 上
                    deps.push(depsMap.get(ITERATE_KEY));
                    
                    // 如果 target 是一個 Map,就會觸發 MAP_KEY_ITERATE_KEY
                    if (isMap(target)) {
                        // MAP_KEY_ITERATE_KEY 同上面的 ITERATE_KEY 一樣
                        // 不同的是,它是用來標識 Map 的迭代器
                        // 例如:Map.prototype.keys()、Map.prototype.values()、Map.prototype.entries()
                        deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
                    }
                }
                
                // 如果 key 是一個數字,就會觸發 length 依賴
                else if (isIntegerKey(key)) {
                    // 因為陣列的索引是可以透過 arr[0] 這種方式來訪問的
                    // 也可以透過這種方式來修改陣列的值,所以會觸發 length 依賴
                    deps.push(depsMap.get('length'));
                }
                break;
                
            // 如果 type 為 delete,就會觸發的依賴變更
            case "delete" /* TriggerOpTypes.DELETE */:
                // 如果 target 不是陣列,就會觸發迭代器,同上面的 add 操作
                if (!isArray(target)) {
                    deps.push(depsMap.get(ITERATE_KEY));
                    if (isMap(target)) {
                        deps.push(depsMap.get(MAP_KEY_ITERATE_KEY));
                    }
                }
                break;
                
            // 如果 type 為 set,就會觸發的依賴變更
            case "set" /* TriggerOpTypes.SET */:
                // 如果 target 是一個 Map,就會觸發迭代器,同上面的 add 操作
                if (isMap(target)) {
                    deps.push(depsMap.get(ITERATE_KEY));
                }
                break;
        }
    }
    
    // 建立一個 eventInfo 物件,主要是除錯的時候會用到
    const eventInfo = {
        target,
        type,
        key,
        newValue,
        oldValue,
        oldTarget
    };
    
    // 如果 deps 的長度為 1,就會直接執行
    if (deps.length === 1) {
        if (deps[0]) {
            {
                triggerEffects(deps[0], eventInfo);
            }
        }
    }
    else {
        // 如果 deps 的長度大於 1,這個時候會組裝成一個陣列,然後再執行
        // 這個時候呼叫就類似一個呼叫棧
        const effects = [];
        for (const dep of deps) {
            if (dep) {
                effects.push(...dep);
            }
        }
        {
            triggerEffects(createDep(effects), eventInfo);
        }
    }
}

tigger函式的作用就是觸發依賴,當我們修改資料的時候,就會觸發依賴,然後執行依賴中的副作用函式。

在這裡的實現其實並沒有執行,主要是收集一些需要執行的副作用函式,然後在丟給triggerEffects函式去執行。

這裡的難點在於區分不同的操作型別,然後收集不同的副作用函式,並且需要理解為什麼要這樣區分;

主要是這節寫的有點多,所以這一塊暫時不在這裡展開,後面會單獨寫一篇文章來講解。

現在我們來看看triggerEffects函式:

function triggerEffects(dep, debuggerEventExtraInfo) {
    // 如果 dep 不是陣列,就會將 dep 轉換成陣列,因為這裡的 dep 可能是一個 Set 物件
    const effects = isArray(dep) ? dep : [...dep];
    
    // 執行 computed 依賴
    for (const effect of effects) {
        if (effect.computed) {
            triggerEffect(effect, debuggerEventExtraInfo);
        }
    }
    
    // 執行其他依賴
    for (const effect of effects) {
        if (!effect.computed) {
            triggerEffect(effect, debuggerEventExtraInfo);
        }
    }
}

這裡沒什麼特殊的,就是轉換一下dep,然後執行computed依賴和其他依賴,主要還是在triggerEffect函式:

function triggerEffect(effect, debuggerEventExtraInfo) {
    // 如果 effect 不是 activeEffect,或者 effect 允許遞迴,就會執行
    if (effect !== activeEffect || effect.allowRecurse) {
        
        // 如果 effect.onTrigger 存在,就會執行,只有開發模式下才會執行
        if (effect.onTrigger) {
            effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
        }
        
        // 如果 effect 是一個排程器,就會執行 scheduler
        if (effect.scheduler) {
            effect.scheduler();
        }
        
        // 否則直接執行 effect.run()
        else {
            effect.run();
        }
    }
}

這裡的邏輯也很簡單,但是如果結合effect函式,就會發現這裡的實現非常的巧妙。

這裡的 effect.schedulereffect.run,在我們看effect函式的時候,就已經出現過了;

run就是呼叫副作用函式,scheduler是排程器,允許使用者自定義呼叫副作用函式的時機。

還是因為這一篇寫的太多了,所以這裡就不展開了,後面會單獨寫一篇文章來講解。

動手時間

上面講了那麼多,還不如自己動一下手來實現這一整套流程,這樣才能更好的理解。

首先我們梳理一下整個流程:

  1. 建立一個響應式物件
  2. 建立一個副作用函式
  3. 訪問響應式物件,觸發依賴收集
  4. 修改響應式物件,觸發依賴執行
// 1. 建立一個響應式物件
const state = reactive({
    name: '田八',
    age: 18
});

// 2. 建立一個副作用函式
effect(() => {
    // 3. 訪問響應式物件,觸發依賴收集
    console.log(state.name);
});

// 4. 修改響應式物件,觸發依賴執行
state.age = 19;

建立一個響應式物件

function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            // 依賴收集
            track(target, key);
            return Reflect.get(target, key);
        },
        set(target, key, value) {
            const res = Reflect.set(target, key, value);
            // 依賴觸發
            trigger(target, key);

            return res;
        }
    });
}

這裡只做最簡單的實現,所以沒有做深度監聽,只是簡單的監聽了一層,並且只有getset兩個鉤子,只對Object型別的資料做了監聽。

建立一個副作用函式

let activeEffect = null;
function effect(fn) {
    const _effect = new ReactiveEffect(fn);
    _effect.run();
}

class ReactiveEffect {
    constructor(fn) {
        this.fn = fn;
        this.deps = [];
    }
    
    run() {
        activeEffect = this;
        this.fn();
        activeEffect = null;
    }
}

這裡的ReactiveEffect類,主要是用來儲存副作用函式的,然後在run函式中,將activeEffect設定為當前的ReactiveEffect例項,這樣在track函式中,就可以拿到當前的ReactiveEffect例項。

依賴收集

const targetMap = new WeakMap();
function track(target, key) {
    if (activeEffect) {
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
        }
        let dep = depsMap.get(key);
        if (!dep) {
            depsMap.set(key, (dep = new Set()));
        }
        if (!dep.has(activeEffect)) {
            dep.add(activeEffect);
            activeEffect.deps.push(dep);
        }
    }
}

這裡的主流程和Vue3的原始碼是一樣的,並沒有做什麼改動,確實是非常的巧妙。

依賴觸發

function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) {
        return;
    }
    const dep = depsMap.get(key);
    if (dep) {
        dep.forEach(effect => {
            effect.run();
        });
    }
}

這裡簡化了流程,直接遍歷dep,然後執行effectrun函式。

線上地址:https://codesandbox.io/s/magical-knuth-mjyh7j?file=/src/main.js

總結

這一篇文章,主要是講解了Vue3的響應式原理,以及如何手動實現一個簡單的響應式系統。

整個響應式系統的實現,主要是圍繞的effect函式,reactive函式,track函式,trigger函式這四個函式。

每個函式都只做自己的事情,各司其職:

  • effect函式:建立一個副作用函式,主要的作用是來執行副作用函式
  • reactive函式:建立一個響應式物件,主要的作用是來監聽物件的變化
  • track函式:依賴收集,主要收集的就是effect函式
  • trigger函式:依賴觸發,主要的作用是來觸發track函式收集的effect函式

這樣的設計,讓整個響應式系統的實現變得非常的簡單,也讓整個系統的可維護性變得非常的高。

這裡的巧妙點在於依賴收集,當呼叫副作用函式時,副作用函式裡面的響應式物件在呼叫時,會觸發get鉤子;

get中呼叫track函式收集activeEffect,這個時候activeEffect是一定存在的,並且activeEffect中的副作用函式是一定引用了這個響應式物件的,所以這個時候就可以將這個響應式物件和activeEffect關聯起來。

將當前的物件作為key,將activeEffect作為value,儲存到targetMap中,這樣就完成了依賴收集。

在響應式物件的set鉤子中,呼叫trigger函式,將targetMap中的activeEffect取出來,然後執行activeEffectrun函式,這樣就完成了依賴觸發。

今天就到了這裡,如有不對的地方,歡迎大家指正。

大家好,這裡是田八的【原始碼&庫】系列,Vue3的原始碼閱讀計劃,Vue3的原始碼閱讀計劃不出意外每週一更,歡迎大家關注。

如果想一起交流的話,可以點選這裡一起共同交流成長

首發在掘金,無任何引流的意思,後續文章不再強調。

系列章節:

相關文章