1. 程式人生 > 實用技巧 >建立一個簡單的迷你Vue3-1

建立一個簡單的迷你Vue3-1

Reactivity響應式資料系統

經過前面第一章,我們已經建好了渲染系統和Vdom相關。

那第二步則是構建雙向繫結的響應式資料系統,我們知道在Vue2中資料雙向繫結是通過Object.defineProperty來實現的,通過這個API來實現的資料雙向繫結有一些缺點。主要是:

i. 所有需要進行繫結觀察的物件屬性必須要先定義好,因為在實現上是通過遍歷所有key來修改getter和setter方法來實現的,所以新增的屬性就沒辦法響應式了,除非手動呼叫

方法來進行處理

ii. 陣列類的操作因為不會觸發getter和setter,因此Vue2是重寫了Array的push,splice等方法來實現的響應式資料,因此直接修改陣列指定index的item時候無法觸發響應式的更新,

並且重寫陣列原生方法也可能帶來一些意想不到的問題。

基於此,Vue3使用ES6新增的Proxy屬性來實現響應式資料繫結,上述所提到的兩個問題都解決了,唯一的缺點就是IE系列低版本並不支援這個特性。當然我們因為業務都是在移動端的,

因此並沒有這個情況。這個情況應該是Vue3框架他們應當考慮的問題了。關於Proxy可以前往MDN參考Proxy特性

介紹完了基本的情況,下面我們就來開始實現它:

思路:

要想實現一個雙向資料繫結,並且在資料更新的時候能夠通知到使用方,那麼我們肯定得在資料讀取和設定的兩個地方都要有狗子,當我們讀取資料的時候,將資料的依賴函式註冊,當我們

設定資料的時候,將新的值作為引數傳遞給註冊的函式並呼叫。這樣就實現了響應式的資料系統。

實現:

一、建立依賴關係管理類Dep
<html>
    <head>
        <title>Mini Vue3</title>
    </head>
    <body>
        <script>
            let activeEffect = null;

            class Dep {
                // 建構函式
                constructor (value) {
                    // 初始化訂閱列表
this.subscribers = new Set(); // 儲存需要跟蹤依賴的值 this._value = value; } get value() { // 取值時新增依賴 this.depend(); return this._value; } set value(newValue) { // 設定時通知更新 this._value = newValue; this.notify(); } // 新增依賴 depend() { if (activeEffect) { this.subscribers.add(activeEffect); } } // 依賴更新,通知回撥 notify() { this.subscribers.forEach(effect => { effect(); }); } } /** * @ddecription 註冊事件,當 */ function watchEffect(effect) { activeEffect = effect; effect(); activeEffect = null; } const dep = new Dep('hello'); // 註冊依賴 watchEffect(() => { console.log(dep.value); }); // 依賴更新 dep.value = 'changed'; </script> </body> </html>

這個依賴關係類起什麼作用的呢,我們看到其中有儲存註冊回撥的subscribes,新增依賴的depend,以及通知依賴更新的notify。

當我們在get value時註冊依賴,將watchEffect中的effect回撥儲存下來。然後在set value的時候通知回撥執行。這樣我們就實現了一個自動管理依賴的class了。

那麼這裡的watchEffect是個什麼函式呢,跟之前Vue2中的watch有什麼區別呢?

可以看下目前Vue3的組合式API草案文件中關於watchEffect的描述:watchEffect

與之前watch不同的是,這個會立即執行,不需要immediate引數。並且可以在任意地方引入。

可以看到已經實現了我們的目的,立即執行輸出hello,並且在value改變之後觸發了執行輸出了changed。

因此此時已經建立依賴關係類Dep,但是我們在實際Vue3中用到的則是對一個物件建立響應式觀察用的是reactive方法,具體可以參考Reactive

因為這裡的Dep我們應當是用來作為依賴管理類,而不是監控自身的值。對於物件值的處理應當由reactive方法處理,因此我們來實現reactive方法如下:

<html>
    <head>
        <title>Mini Vue3</title>
    </head>
    <body>
        <script>
            let activeEffect = null;

            class Dep {
                subscribers = new Set();

                // 新增依賴
                depend() {
                    if (activeEffect) {
                        this.subscribers.add(activeEffect);
                    }
                }

                // 依賴更新,通知回撥
                notify() {
                    this.subscribers.forEach(effect => {
                        effect();
                    });
                }
            }

            /**
             * @decription 註冊事件,當依賴的物件發生改變時,觸發effect方法執行
             */
            function watchEffect(effect) {
                activeEffect = effect;
                effect();
                activeEffect = null;
            }

            /**
             * @description 給物件封裝並返回響應式代理
             */
            function reactive(oldObj) {

            }

            // 需要實現的效果類似於
            const state = reactive({
                count: 0
            });

            watchEffect(() => {
                console.log(state.count);
            });
            // 第一次應當輸出0

            state.count += 1;
            // 此時應當輸出1
        </script>
    </body>
</html>

我們應當要實現這樣的效果。接下來我們就要填充reactive方法

如果是在Vue2,那麼方法類似這樣:

            /**
             * @description 給物件封裝並返回響應式代理
             */
            function reactive(oldObj) {
                Object.keys(oldObj).forEach(key => {
                    const dep = new Dep();
                    let value = oldObj[key];

                    Object.defineProperty(oldObj, key, {
                        get () {
                            // 註冊依賴
                            dep.depend();
                            return value;
                        },
                        set (newValue) {
                            value = newValue;
                            // 通知依賴更新
                            dep.notify();
                        }
                    });

                    return oldObj;
                });
            }

這也是為什麼在Vue2中新增的屬性不會自動加入響應式,而需要通過Vue.set手工呼叫。

而在Vue3中使用的是Proxy,則沒有這個問題了。所以在Vue3中的reactive方法實現如下:

<html>
    <head>
        <title>Mini Vue3</title>
    </head>
    <body>
        <script>
            let activeEffect = null;

            class Dep {
                subscribers = new Set();

                // 新增依賴
                depend() {
                    if (activeEffect) {
                        this.subscribers.add(activeEffect);
                    }
                }

                // 依賴更新,通知回撥
                notify() {
                    this.subscribers.forEach(effect => {
                        effect();
                    });
                }
            }

            /**
             * @decription 註冊事件,當依賴的物件發生改變時,觸發effect方法執行
             */
            function watchEffect(effect) {
                activeEffect = effect;
                effect();
                activeEffect = null;
            }

            const reactiveHandlers = {
                get(target, key, receiver) {
                    
                },
                set(target, key, value, receiver) {

                }
            };

            /**
             * @description 給物件封裝並返回響應式代理
             */
            function reactive(oldObj) {
                return new Proxy(oldObj, reactiveHandlers);
            }

            // 需要實現的效果類似於
            const state = reactive({
                count: 0
            });

            watchEffect(() => {
                console.log(state.count);
            });
            // 第一次應當輸出0

            state.count += 1;
            // 此時應當輸出1
        </script>
    </body>
</html>

但是這裡有個問題,因為reactiveHandlers作為公共handler定義了,那麼每個物件的依賴要如何管理呢?

我們通過一個weakMap來管理所有的依賴關係。之所以使用weakMap,因為weakMap只能使用物件做key,所以不能遍歷.因此當物件被回收之後,map內的引用全無,因此map也能夠被回收,這是Vue3這裡之所以使用weakMap的原因。

新的程式碼如下:

<html>
    <head>
        <title>Mini Vue3</title>
    </head>
    <body>
        <script>
            let activeEffect = null;

            class Dep {
                subscribers = new Set();

                // 新增依賴
                depend() {
                    if (activeEffect) {
                        this.subscribers.add(activeEffect);
                    }
                }

                // 依賴更新,通知回撥
                notify() {
                    this.subscribers.forEach(effect => {
                        effect();
                    });
                }
            }

            /**
             * @decription 註冊事件,當依賴的物件發生改變時,觸發effect方法執行
             */
            function watchEffect(effect) {
                activeEffect = effect;
                effect();
                activeEffect = null;
            }

            const targetMap = new WeakMap();

            function getDep(target, key) {
                // 新增整個物件的依賴
                let currMap = targetMap.get(target);

                if (!currMap) {
                    currMap = new Map();
                    targetMap.set(target, currMap);
                }

                // 獲取具體key的依賴
                let dep = currMap.get(key);
                // 如果沒有新增過則新增依賴
                if (!dep) {
                    dep = new Dep();
                    currMap.set(key, dep);
                }

                return dep;
            }

            const reactiveHandlers = {
                get(target, key, receiver) {
                    const dep = getDep(target, key);

                    // 為什麼每次讀取都要新增依賴,因為有可能會增加新的依賴
                    dep.depend();
                    return Reflect.get(target, key, receiver);
                },
                set(target, key, value, receiver) {                    
                    const dep = getDep(target, key);

                    // 類似於之前的Object.set
                    const ret = Reflect.set(target, key, value, receiver);

                    // 通知執行依賴
                    dep.notify();

                    // 因為proxy的set必須要返回一個boolean的值告訴設定成功與否,因此這裡返回系統api的結果即可
                    return ret;
                }
            };

            /**
             * @description 給物件封裝並返回響應式代理
             */
            function reactive(oldObj) {
                return new Proxy(oldObj, reactiveHandlers);
            }

            // 需要實現的效果類似於
            const state = reactive({
                count: 0
            });

            watchEffect(() => {
                console.log(state.count);
            });
            // 第一次應當輸出0

            state.count += 1;
            // 此時應當輸出1
        </script>
    </body>
</html>

可以看到效果:

沒有問題,關於程式碼中所使用的Reflect可能有些同學會有疑問,相關內容可以參見Reflect,需要注意的是,因為基於Proxy的響應式資料的響應式其實是在物件上,而非Vue2的物件屬性,因此新增屬性也沒有問題。而且利用Proxy的handler中的方法,除了get和set,連判斷屬性是否存在的has等方法也都可以追蹤和處理依賴,具體的可以去閱讀前面所發的Proxy的MDN資料。

當然這裡寫的都是極為簡單的沒有考慮邊界情況。但是畢竟我們所需要的是瞭解核心機制,邊界情況並非核心機制了