上一次我們提到:
- 每個物件的每個屬性都需要自己的 Dep。
- 如何建立
target.a
→ Dep 的對應關係? - 如何在不污染原始物件的情況下儲存這個關係?
我們可以先來做一個簡單的比較
Ref、Reactive 比較
Ref | Reactive | |
---|---|---|
資料結構 | 單一值 | 物件(多個屬性) |
依賴儲存 | 直接在實例上(this) | 需要一個外部的的儲存機制 |
一個 ref | 一個 Dep | 多個 Dep(每個屬性一個) |
Ref 可以用 this
因為它就是一個實例。
但 Reactive 的每個屬性都需要自己的 Dep,要存在哪?
那我們這時候可以建立一個 Weak Map
物件。
什麼是 Weak Map?
WeakMap
是一種 鍵值對集合(key-value pairs)。- key 只能是物件(不能是字串、數字、布林),value 可以是任意型別。
- 弱引用(weak reference):如果一個物件只被 WeakMap 當 key 使用,而程式中沒有其它變數參考它,這個物件就會被垃圾回收(GC)自動清掉。
看來來正好適合我們去做關聯關係。
核心概念
WeakMap
建立一個全域的 targetMap
,它的三層巢狀結構如下:
- 第一層
targetMap
(WeakMap) :key
是原始的目標物件target
,value
是第二層的depsMap
。{ target => depsMap }
- 第二層
depsMap
(Map) :key
是target
物件中的屬性名key
,value
是第三層的dep
。{ key => dep }
- 第三層
dep
(Dep 實例) :依賴的容器,儲存了所有訂閱該屬性變更的effect
。{ subs, subsTail }
為何不直接用 Map?
因為如果使用一般的 Map,Map 會一直保持對 target 物件的引用,只要它還存在於 Map 中,GC 就無法回收,導致記憶體洩漏。
1const targetMap = new WeakMap()
它的結構會長這樣
1target = {2 a:0,3 b:14}5
6tagetMap = {7 [obj]:{8 a:Dep,9 b:Dep10 }11}
這樣子 Dep 跟 target 就有關係了,一個屬性對應一個 Dep,我們可以通過 target 找到 obj 對應的物件,還可以透過屬性a找到Dep實例,這樣就可以建立關聯關係。
targetMap
的 key 是 obj ,value 是一個 map,這個 map 裡面,map 裡面的 key 就是 obj 的屬性,value 就是對應的 dep,這樣就可以收集依賴。
等到需要觸發更新,透過 obj 找到對應的 Map,再透過 key 找到對應的 Dep 通知更新。
收集依賴
收集依賴有分為首次收集依賴,跟之前已經收集過了,所以我們可以這樣寫。
1function track(target, key){2 if(!activeSub)return3 // 透過 targetMap 取得 target 的依賴4 let depsMap = targetMap.get(target)5
6 //首次收集依賴,之前沒有收集過,就新建一個7 // key:obj / value:depsMap8 if(!depsMap){9 depsMap = new Map()10 targetMap.set(target, depsMap)11 }12
13 let dep = depsMap.get(key)14
15 // 收集依賴:第一次建立物件依賴關聯,並且保存到depsMap中10 collapsed lines
16 // key:key / value:Dep17 if(!dep){18 dep = new Dep()19 depsMap.set(key, dep)20 }21
22 console.log(targetMap, dep)23
24 link(dep, activeSub)25}
可以 console.log
看起來targetMap
跟dep
:
看起來的確是我們想的那樣,接下來做觸發更新。
觸發更新
觸發更新的話,原本是寫去找依賴 (dep
)的 effect
(sub),如果找到,就傳入propagate
,現在我們的 dep
都存入了depsMap
,那我們就理應去depsMap
找:
1function trigger(target, key){2 const depsMap = targetMap.get(target)3 // 如果 depsMap 不存在,表示沒有收集過依賴,直接返回4 if(!depsMap)return5
6 const dep = depsMap.get(key)7 // 如果依賴不存在,表示這個 key 沒有在effect中被使用過,直接返回8 if(!dep)return9
10 // 找到依賴,觸發更新11 propagate(dep.subs)12}
接下來回去看我們的範例,初始化成功輸出0,一秒之後輸出1。
看起來成功了,接下來我們來測試 reactive
中 getter
的響應追蹤:
1//import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'2import { reactive, effect } from '../dist/reactivity.esm.js'3
4const state = reactive({5 a: 0,6 get count(){7 return this.a8 }9})10effect(() => {11 console.log(state.count)12})13
14setTimeout(() => {15 state.a = 11 collapsed line
16}, 1000)
預期結果是先輸出 0,一秒後輸出 1。但實際執行後,我們發現只有初始的 0 被輸出,state.a = 1
的更新並未觸發 effect
。
透過在 track
函式中輸出 (target, key)
,發現只有 count
屬性被追蹤了,a
屬性並沒有。
原因在於 return this.a
上,在 getter
內部,this
預設指向的是原始的 target
物件,而不是我們的 proxy
物件。
因此 this.a
的取值過程繞過了 Proxy
的 get
,a
屬性的依賴自然也沒辦法收集。
1const state = reactive({2 a: 0,3 get count() {4 return this.a // this 應該指向誰?5 }6})
它應該要指向我們的 Proxy 物件而不是原始物件,這樣它在觸發getter
的時候,才會執行track(target, key)
。
那我們要怎麼做?
Proxy
的 handler
提供第三個參數 receiver
,它指向的就是 proxy
物件本身,因此我們只需要將它傳遞給 Reflect.get
就可以修正 this
的指向。
1function createReactiveObject(target) {2 // reactive 只處理物件3 if (!isObject(target)) return target4
5 // 建立 target 的代理物件6 const proxy = new Proxy(target, {7 get(target, key, receiver) {8 // 收集依賴:綁定target的屬性與effect的關係9 track(target, key)10 return Reflect.get(target, key,receiver)11 },12 set(target, key, newValue, receiver) {13 const res = Reflect.set(target, key, newValue, receiver)14 // 觸發更新:通知之前收集的依賴,重新執行effect15 trigger(target, key)6 collapsed lines
16 return res17 }18 })19
20 return proxy21}
這樣我們就完成,也可以看到初始化 console.log
輸出0,一秒之後輸出1。
執行步驟
回顧我們今天:
- 我們引入了以
WeakMap
為核心的targetMap
資料結構,解決在不污染原始物件的前提下,為多屬性物件管理各自依賴。 - 我們實作與
targetMap
配套的track
和trigger
函式。 - 我們利用
Proxy
的receiver
參數,修正getter
中this
指向的問題。