「Vue 設計與實現」響應系統原理(三)- 更完善的副作用桶子
「Vue.js 設計與實現」之讀書筆記與整理 - 更完善的副作用桶子
為了解決桶不夠完善的問題,需要把結構調整一下,原本只是簡單的 Set 收集副作用,現在需要調整為把各個物件、屬性、副作用分別收集。
// 單層
effectFn
target
└── key
└── effectFn
修改前的桶
只有 Set 的單層結構,每個物件、屬性被讀取(get),都會被收集到同一個 Set 內,當任一個屬性被設值(set),就會把所有的副作用拿出來執行,這樣是不合理的。
在下面的程式碼中,我們使用 new Set 當作桶子,在 setTimeout 2 秒後,修改了不存在的屬性 proxy.notExist,卻觸發了副作用函式的執行,log 了 'effect run',但是 notExist 這個屬性是沒有在任何地方被讀取的,不需要執行副作用。當 proxy 越來越龐大,這會是一個效能問題。
/**
* 副作用函式
*/
let activeEffect
/**
* 註冊副作用的函式
* effect => effectRegister
*/
function effectRegister(fn) {
activeEffect = fn
fn()
}
const bucket = new Set()
const data = { text: 'hello world', age: 22 }
const proxy = new Proxy(data, {
get(target, key) {
if (activeEffect)
bucket.add(activeEffect)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
bucket.forEach(fn => fn())
return true
},
})
effectRegister(() => {
console.log('effect run')
document.body.innerText = proxy.text
})
setTimeout(() => {
proxy.notExist = 'goodbye'
// 這邊讀取不存在的屬性,仍會執行整個 proxy 副作用收集的回調 `bucket.forEach(fn => fn()`
}, 2000)
修改後的桶
有以下的結構
- 用 WeakMap 把目標物件當作 key,使用 Map 當作 value
- Map 當中使用目標屬性的 key 當作 key,使用 Set 來當作 value
- value 內收集副作用函式

如此一來,在新的桶的設計當中,若任一個屬性沒有被讀取過,在 js 內去修改該屬性,也不會觸發副作用函式。
/**
* 副作用函式
*/
let activeEffect
/**
* 註冊副作用的函式
* effect => effectRegister
*/
function effectRegister(fn) {
activeEffect = fn
fn()
}
const data = { text: 'hello world', age: 22 }
const bucket = new WeakMap()
const proxy = new Proxy(data, {
get(target, key) {
if (!activeEffect)
return
const _depsMap = bucket.get(target)
const hasDepsMap = !!_depsMap
// 檢查是否有對應的 Map,沒有就創建一個新的
const depsMap = hasDepsMap ? _depsMap : new Map()
!hasDepsMap && bucket.set(target, depsMap)
const _deps = depsMap.get(key)
const hasDeps = !!_deps
// 檢查是否有對應的 Set,沒有就創建一個新的
const deps = hasDeps ? _deps : new Set()
!hasDeps && depsMap.set(key, deps)
deps.add(activeEffect)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
const effects = bucket.get(target)?.get(key)
effects && effects.forEach(fn => fn())
return true
},
})
effectRegister(() => {
console.log('effect run')
document.body.innerText = proxy.text
})
setTimeout(() => {
proxy.noExist = '222'
}, 2000)
為什麼使用 WeakMap
主要原因是怕記憶體流失(Memory Leak),WeakMap 具有較好的垃圾回收機制,這邊需要提一下 WeakMap & Map 的差異:
const map = new Map()
const weakMap = new WeakMap();
(function () {
const foo = { foo: 1 }
const bar = { bar: 2 }
map.set(foo, 1)
weakMap.set(bar, 2)
})()
在上方的程式碼中,map & weakMap 分別在 IIFE 執行當中,將 foo, bar 當作 key 引用,當函式執行完畢,foo 這個物件,因為仍被 map 引用,所以垃圾回收器(grabage collector)不会把它從內存記憶體中移除。仍然可以通過 map.keys 列出 foo 物件,但是 bar 物件在函式執行完畢後就會被釋放掉,因為 WeakMap 的 key 是弱引用,不會影響垃圾回收器的工作。
- WeakMap 經常用於儲存那些只有當 key 所引用的物件存在時(没有被回收)才有價值的資料。
- WeakMap 的 weak 就是弱引用的意思
在桶的設計當中,如果 target 物件沒有任何引用了,說明 client side 已經不需要它了,就讓垃圾回收器自動運作完成回收任務。如果使用 Map 來代替 WeakMap,即使 client side 對 target 沒有任何引用,這個物件也不會被回收,最終會導致 Memory Leak。
封裝 track & trigger 函數
- 將副作用的收集封裝為 track 函數,在 get 函數中調用 track 函數追蹤變化
- 將副作用的執行為 trigger 函數,在 set 函數内調用 trigger 函數觸發變化
這樣一來,就可以提高對 track, trigger 的擴展性。
封裝 track, trigger 函數 - stackblitz
/**
* 副作用函式
*/
let activeEffect
/**
* 註冊副作用的函式
* effect => effectRegister
*/
function effectRegister(fn) {
activeEffect = fn
fn()
}
const data = { text: 'hello world', age: 22 }
const bucket = new WeakMap()
const proxy = new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
return true
},
})
// 在 get 函數中調用 track 函數追蹤變化
function track(target, key) {
if (!activeEffect)
return
const _depsMap = bucket.get(target)
const hasDepsMap = !!_depsMap
// 檢查是否有對應的 Map,沒有就創建一個新的
const depsMap = hasDepsMap ? _depsMap : new Map()
!hasDepsMap && bucket.set(target, depsMap)
const _deps = depsMap.get(key)
const hasDeps = !!_deps
// 檢查是否有對應的 Set,沒有就創建一個新的
const deps = hasDeps ? _deps : new Set()
!hasDeps && depsMap.set(key, deps)
deps.add(activeEffect)
}
// 在 set 函數内調用 trigger 函數觸發變化
function trigger(target, key) {
const effects = bucket.get(target)?.get(key)
effects && effects.forEach(fn => fn())
}
effectRegister(() => {
console.log('effect run')
document.body.innerText = proxy.text
})
setTimeout(() => {
proxy.text = '222'
}, 2000)