Vue3響應式的簡單實現


vue3與vue2響應式的區別

1.vue2響應式

  • 數據在data中注冊,編譯時直接將data中的所有數據綁定監聽
  • 利用Object.defineProperyty()監聽數據的get和set
  • 用Observe,Dep,Watcher三個類實現依賴收集

缺點:對於在html中未使用的數據也設置了監聽,需要對每一個基本數據類型都要設置劫持,defineProperty監聽不到數組/對象內部變化,同時多次調用observe函數進行遞歸,性能不高。

2.vue3響應式

  • 利用reactive注冊響應式對象,對函數返回值操作
  • 利用Proxy劫持數據的get,set,deleteProperty,has,own
  • 利用WeakMap,Map,Set來實現依賴收集

缺點:使用大量ES6新增特性,舊版本瀏覽器兼容性差。

Proxy與Reflect

Proxy和Reflect是ES6新增的兩個類,Proxy相比Object.defineProperty更加好用,解決了后者不能監聽數組改變的缺點,並且還支持劫持整個對象,並返回一個新對象,不管是操作便利程度還是底層功能上都遠強於Object.defineProperty,Reflect的作用是可以拿到Object內部的方法,並且在操作對象出錯時返回false不會報錯。

實現

主要分為兩大步,設置響應式對象和依賴收集(發布訂閱):
1.設置響應式對象
首先創建Proxy,傳入將要監聽的對象,然后通過handler設置對象的監聽,通過get等函數的形參對數據進行劫持處理,然后創建兩個WeakMap實例toProxy,toRow來記錄當前對象的代理狀態,防止重復代理,在set函數中,通過判斷屬性的類別(新增屬性/修改屬性)來減少不必要的操作。

/* ----------------響應式對象---------------- */
function reactive(target) {
    /* 創建響應式對象 */
    return createReactiveObject(target);
}
/* 防止重復設置代理(target,observer) */
let toProxy = new WeakMap();
/* 防止重復被代理(observer,target) */
let toRow = new WeakMap();
/* 設置響應監聽 */
function createReactiveObject(target) {
    /* 非對象或被代理過則直接返回 */
    if (!isObject(target) || toRow.has(target)) return target;
    /* 已經有代理則直接返回 */
    let proxy = toProxy.get(target);
    if (proxy) {
        return proxy;
    }
    /* 監聽 */
    let handler = {
        get(target, key) {
            console.log(`get---key(${key})`);
            let res = Reflect.get(target, key);
            /* 添加追蹤 */
            track(target, key);
            /* 如果是對象則繼續往下設置響應 */
            return isObject(res) ? reactive(res) : res;
        },/* 獲取屬性 */
        set(target, key, val, receiver) {
            console.log(`set---key(${key})`);
            /* 判斷是否為新增屬性 */
            let hasKey = hasOwn(target, key);
            /* 存儲舊值用於比對 */
            let oldVal = target[key];
            let res = Reflect.set(target, key, val, receiver);
            if (!hasKey) {
                console.log(`新增屬性---key(${key})`);
                /* 調用追蹤器,綁定新增屬性 */
                track(target, key);
                /* 調用觸發器,更改視圖 */
                trigger(target, key);
            } else if (val !== oldVal) {
                console.log(`修改屬性---key(${key})`);
                trigger(target, key);
            }
            return res;
        },/* 修改屬性 */
        deleteProperty(target, key) {
            console.log(`delete---key(${key})`);
            let res = Reflect.deleteProperty(target, key);
            return res;
        }/* 刪除屬性 */
    }
    /* 創建代理 */
    let observer = new Proxy(target, handler);
    /* 記錄與target的聯系 */
    toProxy.set(target, observer);
    toRow.set(observer, target);
    return observer;
}

2.收集依賴(發布訂閱)
vue3中主要通過嵌套的方式實現,原理如下圖:
vue3響應式
每次向effect函數傳入一個fun----console.log(person.name)后,會先執行一遍run函數,將effect推入棧中,然后執行fun,在執行fun的過程中,會讀取person對象,進而觸發get函數

/* 事件棧 */
let effectStack = [];
/* ----effect函數---- */
function effect(fun) {
    /* 將fun壓入棧 */
    let effect = createReactiveEffect(fun);
    /* 初始化執行一次 */
    effect();//實際上是運行run
}
function createReactiveEffect(fun) {
    /* 創建響應式effect */
    let effect = function () {
        return run(effect, fun);
    }
    return effect;
}
function run(effect, fun) {
    /* 防止報錯導致棧內元素無法彈出 */
    try {
        effectStack.push(effect);
        fun();
    } finally {
        effectStack.pop();
    }
}

get函數調用追蹤器track並傳入(person,name),在track中先獲取棧頂元素,也就是剛剛觸發的fun,假設當前targetsMap是空的,那么此時將會創建一個新的映射target->new Map(),此時depsMap必然也要創建一個新的映射,把key映射到new Set(),然后向key對應的deps中放入effect,此時,name和fun函數之間的綁定已經實現,執行完后effectStack將會把fun函數彈出,防止越堆越多。

/* 目標Map */
let targetsMap = new WeakMap();
/* ----追蹤器---- */
function track(target, key) {
    /* 獲取觸發track的事件 */
    let effect = effectStack[effectStack.length - 1];
    if (effect) {
        /* 獲取以target作為標識的depsMap */
        let depsMap = targetsMap.get(target);
        if (!depsMap) {
            /* 如果不存在就創建一個新Map */
            targetsMap.set(target, depsMap = new Map());
        }
        /* 獲取以key為標識的deps */
        let deps = depsMap.get(key);
        if (!deps) {
            depsMap.set(key, deps = new Set());
        }
        /* 向deps中加入事件 */
        if (!deps.has(effect)) {
            deps.add(effect);
        }
    }
}

接下來是觸發的過程,當每次進行類似person.name='lisi’這樣的改值操作時,就會觸發響應的set函數,set函數對比屬性的新舊值后調用trigger函數將(person,name)傳入,trigger根據兩個傳入值結合targetsMap->depsMap->deps的順序找到name對應的事件數組,然后執行所有事件達到響應更新的目的,至此,簡化版的vue3響應機制就實現了。

/* ----觸發器---- */
function trigger(target, key) {
    /* 獲取depsMap */
    let depsMap = targetsMap.get(target);
    if (depsMap) {
        /* 獲取deps */
        let deps = depsMap.get(key);
        /* 執行deps數組中所有的事件 */
        deps.forEach(effect => {
            effect();
        });
    }
}

代碼:https://gitee.com/aeipyuan/simple_implementation_of_vue3
以上內容均為我個人理解,如有錯誤,歡迎指正。


免責聲明!

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



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