建立一個簡單的迷你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資料。
當然這裡寫的都是極為簡單的沒有考慮邊界情況。但是畢竟我們所需要的是瞭解核心機制,邊界情況並非核心機制了