
上一次我們提到:
target.a → Dep 的對應關係?我們可以先來做一個簡單的比較
| Ref | Reactive | |
|---|---|---|
| 資料結構 | 單一值 | 物件(多個屬性) |
| 依賴儲存 | 直接在實例上(this) | 需要一個外部的的儲存機制 |
| 一個 ref | 一個 Dep | 多個 Dep(每個屬性一個) |
Ref 可以用 this 因為它就是一個實例。
但 Reactive 的每個屬性都需要自己的 Dep,要存在哪?
那我們這時候可以建立一個 Weak Map 物件。
WeakMap 是一種 鍵值對集合(key-value pairs)。看來來正好適合我們去做關聯關係。
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 會一直保持對 target 物件的引用,只要它還存在於 Map 中,GC 就無法回收,導致記憶體洩漏。
const targetMap = new WeakMap()
它的結構會長這樣
target = {
a:0,
b:1
}
tagetMap = {
[obj]:{
a:Dep,
b:Dep
}
}
這樣子 Dep 跟 target 就有關係了,一個屬性對應一個 Dep,我們可以通過 target 找到 obj 對應的物件,還可以透過屬性a找到Dep實例,這樣就可以建立關聯關係。
targetMap 的 key 是 obj ,value 是一個 map,這個 map 裡面,map 裡面的 key 就是 obj 的屬性,value 就是對應的 dep,這樣就可以收集依賴。

等到需要觸發更新,透過 obj 找到對應的 Map,再透過 key 找到對應的 Dep 通知更新。
收集依賴有分為首次收集依賴,跟之前已經收集過了,所以我們可以這樣寫。
function track(target, key){
if(!activeSub)return
// 透過 targetMap 取得 target 的依賴
let depsMap = targetMap.get(target)
//首次收集依賴,之前沒有收集過,就新建一個
// key:obj / value:depsMap
if(!depsMap){
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
// 收集依賴:第一次建立物件依賴關聯,並且保存到depsMap中
// key:key / value:Dep
if(!dep){
dep = new Dep()
depsMap.set(key, dep)
}
console.log(targetMap, dep)
link(dep, activeSub)
}
可以 console.log 看起來targetMap跟dep:

看起來的確是我們想的那樣,接下來做觸發更新。
觸發更新的話,原本是寫去找依賴 (dep)的 effect(sub),如果找到,就傳入propagate,現在我們的 dep 都存入了depsMap,那我們就理應去depsMap找:
function trigger(target, key){
const depsMap = targetMap.get(target)
// 如果 depsMap 不存在,表示沒有收集過依賴,直接返回
if(!depsMap)return
const dep = depsMap.get(key)
// 如果依賴不存在,表示這個 key 沒有在effect中被使用過,直接返回
if(!dep)return
// 找到依賴,觸發更新
propagate(dep.subs)
}
接下來回去看我們的範例,初始化成功輸出0,一秒之後輸出1。

看起來成功了,接下來我們來測試 reactive 中 getter 的響應追蹤:
//import { reactive, effect } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { reactive, effect } from '../dist/reactivity.esm.js'
const state = reactive({
a: 0,
get count(){
return this.a
}
})
effect(() => {
console.log(state.count)
})
setTimeout(() => {
state.a = 1
}, 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 屬性的依賴自然也沒辦法收集。
const state = reactive({
a: 0,
get count() {
return this.a // this 應該指向誰?
}
})
它應該要指向我們的 Proxy 物件而不是原始物件,這樣它在觸發getter的時候,才會執行track(target, key)。
那我們要怎麼做?
Proxy 的 handler 提供第三個參數 receiver,它指向的就是 proxy 物件本身,因此我們只需要將它傳遞給 Reflect.get 就可以修正 this 的指向。
function createReactiveObject(target) {
// reactive 只處理物件
if (!isObject(target)) return target
// 建立 target 的代理物件
const proxy = new Proxy(target, {
get(target, key, receiver) {
// 收集依賴:綁定target的屬性與effect的關係
track(target, key)
return Reflect.get(target, key,receiver)
},
set(target, key, newValue, receiver) {
const res = Reflect.set(target, key, newValue, receiver)
// 觸發更新:通知之前收集的依賴,重新執行effect
trigger(target, key)
return res
}
})
return proxy
}
這樣我們就完成,也可以看到初始化 console.log 輸出0,一秒之後輸出1。

回顧我們今天:
WeakMap 為核心的 targetMap 資料結構,解決在不污染原始物件的前提下,為多屬性物件管理各自依賴。targetMap 配套的 track 和 trigger 函式。Proxy 的 receiver 參數,修正 getter 中 this 指向的問題。同步更新《嘿,日安!》技術部落格