vue3 之 reactive && toRefs原始碼解析

2021-03-28 15:02:27
一、 reactve
  1. 定義
reactive API的定義為傳入一個物件返回一個基於原物件的響應式代理,即返回一個proxy,相當於Vue2.x中的Vue.Observer
  1. 優點

    在Vue2.x中資料的響應式處理是基於Object.defindProperty()的,但他只會偵聽物件的屬性,不會偵聽物件,在新增物件屬性的時候需要:

    Vue.$set(object, 'name', 'lock li')

    reactiveAPI則是基於ES2015 proxy實現了對物件的響應式處理,在vue3.0中往物件中新增屬性,並且這個屬性也會具有響應式的效果:

    object.name = 'lock li'
  2. 注意點

    ① 使用reactiveAPI時,setup中返回的reactive需要通過物件的形式:

    export default definComponent({
        name: 'example',
        setup(props) {
            const obj = reactive({foo: 'bar'})
            
            return {
                obj
            }
        }
    })

    defineComponent

    definComponent主要是用來幫助Vue在TS下正確推斷出setup()元件的引數型別

    export function defineComponent(options: unknown) {
        return isFunction(options) ? { setup: options } : options
    }

    ② 或者藉助toRefsAPI包裹一下匯出,(使用toRefs包裹匯出的我們可以使用展開運運算元或解構接收):

    export default defineComponent({
        setup() {
            let obj = { foo: 'bar' }
            obj = toRefs(obj)
            return {
                ...obj
            }
        }
    })

    二、原始碼實現

    function reactive(target) {
        // if trying to observe a readonly proxy, return the readonly version.
        if (readonlyToRaw.has(target)) {
            return target;
        }
        // target is explicitly marked as readonly by user
        if (readonlyValues.has(target)) {
            return readonly(target);
        }
        if (isRef(target)) {
            return target;
        }
        return createReactiveObject(target, rawToReactive, reactiveToRaw, mutableHandlers, mutableCollectionHandlers);
    }

    可以看到先有3個判斷邏輯,對readonlyToRawreadonlyValuesisRef分別進行了判斷,先不看這些判斷邏輯,通常我們對reactive()傳入一個物件,可以直接命中createReactiveObject()

    createReactiveObject()函數如下:

    function createReactiveObject(target, toProxy, toRaw, baseHandlers, collectionHandlers) {
        if (!isObject(target)) {
            if ((process.env.NODE_ENV !== 'production')) {
                console.warn(`value cannot be made reactive: ${String(target)}`);
            }
            return target;
        }
        // target already has corresponding Proxy
        let observed = toProxy.get(target);
        if (observed !== void 0) {
            return observed;
        }
        // target is already a Proxy
        if (toRaw.has(target)) {
            return target;
        }
        // only a whitelist of value types can be observed.
        if (!canObserve(target)) {
            return target;
        }
        const handlers = collectionTypes.has(target.constructor)
            ? collectionHandlers
            : baseHandlers;
        observed = new Proxy(target, handlers);
        toProxy.set(target, observed);
        toRaw.set(observed, target);
        return observed;
    }

    createReactiveObject()傳入了4個引數,分別是:

    target:我們定義reactive時傳入的物件

    toProxy:一個空的WeakSet

    toRaw:判斷傳入的reactive是否已經被代理

    baseHandlers:一個已經被定義好的的具有getset的物件,它看起來會是這樣子:

    const baseHandlers = {
        get(target, key, value, receiver) {},
        set(target, key, value, receiver) {},
        deleteProxy(target, key) {},
        has(target, key) {},
        ownKey(target) {}
    }
    • collectionHandlers是一個只包含get的物件。

    詳細看createReactiveObject函數,一些分支邏輯先不用管,我們直接看最後的邏輯:

    const handlers = collectionTypes.has(target.constructor)
            ? collectionHandlers
            : baseHandlers;
        observed = new Proxy(target, handlers);
        toProxy.set(target, observed);
        toRaw.set(observed, target);
        return observed;

    首先判斷collectionTypes中是否包含我們傳入的target的建構函式(構造器),而collectionTypes是一個Set集合主要包含:Set Map WeakSet WeakMap 等4種集合的建構函式。

    如果collectionTypes 中包含我們傳入的建構函式,則將handlers賦值為僅包含get屬性的collectionHanders 否則 賦值為baseHandlers

    兩者的區別在於collectionHandlers僅含有get屬性,是用來留給不需要派發更新的變數使用的,例如我們常用的props屬性

    然後將target handlers作為兩個引數傳入,使用替換了vue 2.x中的Object.definePropertyProxy代理函數,並範例化

    接下來兩步也非常重要,將targetobserved 作為鍵值對賦值到toProxy,用於下次檢測傳入的target是否已經被代理,並返回被代理的observed物件:

    // target already has corresponding Proxy
    let observed = toProxy.get(target);
    if (observed !== void 0) {
        return observed;
    }

    observedtarget作為鍵值對賦值到toRaw,用於下次檢測傳入的target是否是一個代理物件,並返回代這個target

    // target is already a Proxy
    if (toRaw.has(target)) {
        return target;
    }

    二、toRefs

    把一個響應式物件轉換成普通物件,該普通物件的每個 property 都是一個 ref ,和響應式物件 property 一一對應。

    使用toRefs其實是為了方便我們使用解構和展開運運算元,具體程式碼:

    function toRefs(object) {
        if ((process.env.NODE_ENV !== 'production') && !isReactive(object)) {
            console.warn(`toRefs() expects a reactive object but received a plain one.`);
        }
        const ret = {};
        for (const key in object) {
            ret[key] = toProxyRef(object, key);
        }
        return ret;
    }
    function toProxyRef(object, key) {
        return {
            _isRef: true,
            get value() {
                return object[key];
            },
            set value(newVal) {
                object[key] = newVal;
            }
        };
    }

    可以看到toRefs是在原有Proxy的基礎上,返回了一個所有屬性帶有getset 的物件,這樣就解決了Proxy物件遇到解構和展開運運算元之後,失去響應性的問題。