上一次我們提到:
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
指向的問題。同步更新《嘿,日安!》技術部落格