使用 Proxy 實現 Vue.js 3 中的響應式思想

咱們知道,Vue.js 2 是經過 Object.defineProperty()函數來實現的響應式。這個月 5 號尤大發布了 Vue.js 3 的源碼,社區立刻出現了不少源碼分享的文章。 你們早就得知新版的響應式是用 Proxy 實現的,如今咱們來利用 Proxy 實現一個基本的響應式骨架。vue

基礎

關於 Proxy 的基礎知識,能夠去MDN學習直達連接react

響應式核心

準備工做

本文將實現響應式的核心函數命名爲 reactive, 這個函數返回一個被代理以後的結果,能夠經過操做這個返回結果來觸發響應式。git

首先,給出測試數據:一個有 name 屬性的對象 obj,咱們但願通過 reactive 函數處理以後返回一個新對象 proxyObj,咱們像操做 obj 同樣操做 proxyObj 對象。數組

let obj = {
    name: 'test name'
}

let proxyObj = reactive(obj);
複製代碼

例如:當修改 proxyObj.name 時會觸發響應式。緩存

其次:設置一個函數用來模擬響應式過程,這裏沒有必要真的去更新DOM。咱們設置一個能夠簡單地輸出一個字符串提示當前須要進行視圖更新的函數就能夠了。函數

// 提示視圖須要更新
function trigger() {
    console.log('視圖須要更新');
}
複製代碼

實現 reactive 函數

一個輔助函數

首先明確 reactive 函數接收一個參數,須要對這個參數進行代理,並返回代理後的結果。若是參數是對象才須要代理,不然直接返回。post

這裏須要創建一個輔助函數用來判斷一個變量是不是對象:學習

function isObject(param) {
    return typeof param === 'object' && param !== null;
}
複製代碼

主體函數

使用 Proxy 時須要定義一個代理對象 handler 來對目標進行代理操做,這個對象主要有兩個方法,即 get 和 set, 分別爲 獲取和設置屬性值的時候觸發。同時在內部的實現須要利用到 Reflect對象, 詳情見下面的代碼。測試

/* 返回一個被代理後的結果,經過操做這個結果能夠來實現響應式, 例如視圖更新 */
function reactive(target) {
    // 若是是個對象,則返回被代理後的結果,若是不是則直接返回
    if(!isObject(target)) {
        return target;
    }
    
    // 須要定義一個代理對象來對 target 進行代理操做
    // 這個對象主要有兩個方法,即 get 和 set
    const handler = {
        get(target, key, receiver) {
            return Reflect.get(target, key, receiver); // 至關於 return target[key]
        },
        set(target, key, value, receiver) {
            trigger();
            return Reflect.set(target, key, value, receiver); // 至關於 target[key] = value
        }
    };

    // 利用 Proxy 來代理這個對象屬性
    let observed = new Proxy(target, handler);

    return observed;
}
複製代碼

如今咱們修改 proxyObj 的name屬性,發現會觸發了最基本的響應式:ui

let obj = {
    name: 'jjjs'
}

let proxyObj = reactive(obj);

// 修改 name 屬性,發現能夠監控到
proxyObj.name = 'new Name';
console.log(proxyObj.name);
複製代碼

結果是輸出:

並且能夠對原本不存在的屬性進行監控:

proxyObj.age = 6;
console.log(proxyObj.age);
複製代碼

結果:

可是當咱們想處理一個數組時,卻發現會觸發兩次視圖更新提示:

let array = [1,2,3];
let obArray = reactive(array);
obArray.push(4)
複製代碼

reactive代碼進行,修改,輸出 set 動做中每次的 key, 觀察是誰觸發了兩次:

function reactive(target) {
    // ...
        set(target, key, value, receiver) {
            trigger();
            console.log(key); // 輸出變更的 key
            return Reflect.set(target, key, value, receiver); // 至關於 target[key] = value
        }
    };
    // ...
}
複製代碼

經過上圖能夠看出當監視數組時,數組下標的更新會觸發一次,而數組 length的更新也會進行觸發,這就是二次觸發的緣由。

但咱們是不須要在 length 更新時對視圖進行更新的,因此須要對這裏的邏輯進行修改:只對私有屬性的修改動做觸發視圖更新。

function reactive(target) {
    const handler = {
        // ...
        set(target, key, value, receiver) {
            // 只對私有屬性的修改動做觸發視圖更新
            if(!target.hasOwnProperty(key)) {
                trigger();
                console.log(key);
            }
            return Reflect.set(target, key, value, receiver); // 至關於 target[key] = value
        }
    };
    // ...
}
複製代碼

當須要對嵌套的對象進行獲取時,例如:

// 對於嵌套的對象
var obj = {
    name: 'jjjs',
    array: [1,2,3]
}

var proxyObj = reactive(obj);
proxyObj.array.push(4);
複製代碼

此時會發現,並不會觸發視圖須要更新的提示,這是須要對對象進行遞歸處理:

function reactive(target) {
// ...
    const handler = {
        get(target, key, receiver) {
            const proxyTarget =  Reflect.get(target, key, receiver); // 至關於獲取 target[key]
            if(isObject(target[key])) { // 對於對象進行遞歸
                return reactive(proxyTarget); // 遞歸
            }

            return proxyTarget;
        },
        // ...
    };
// ...
}
複製代碼

此時發現能夠正常進行監聽:

然而,當屢次獲取代理結果時,會出現屢次觸發代理的狀況:

function reactive(target) {
// ...
    console.log('走代理');
    
    // 利用 Proxy 來代理這個對象屬性
    let observed = new Proxy(target, handler);
    return observed;
}
複製代碼
// 屢次獲取代理結果
var proxyObj = reactive(obj);
var proxyObj = reactive(obj);
var proxyObj = reactive(obj);
var proxyObj = reactive(obj);
複製代碼

結果:

走代理
走代理
走代理
走代理
複製代碼

這種狀況是咱們不但願有的,咱們但願對同一個對象僅作一次代理。這個時候,咱們須要對已經代理過的對象進行緩存,一次在進行代理以前查詢緩存判斷是否已經通過了代理,只有沒有通過代理的對象才走一次代理。

最適合當作緩存容器的對象是 WeakMap, 這是因爲它對於對象的弱引用特性。對WeakMap不熟悉的同窗能夠點擊這裏查看其特性。

如今對代碼進行修改,增長一個緩存對象:

const toProxy = new WeakMap(); // 用來保存代理後的對象

function reactive(target) {
    // ...
    if(toProxy.get(target)) { // 判斷對象是否已經被代理了
        return toProxy.get(target);
    }
    // ...
    console.log('走代理');
    // 利用 Proxy 來代理這個對象屬性
    let observed = new Proxy(target, handler);

    toProxy.set(target, observed); // 保存已經代理了的對象

    return observed;
}
複製代碼

如今觀察上面代碼的結果:

走代理
複製代碼

發現對同一個對象只走了一次代理,這正是咱們指望的結果。

總結

上面僅用了幾十行代碼作出了一個極簡的 Proxy 對於響應式的實現,雖然簡陋,可是對於理解思想足夠了。

源代碼地址: Here

若有錯誤,感謝指出~

相關文章
相關標籤/搜索