「Vue 設計與實現」響應系統原理(五)- 巢狀的 effect
「Vue.js 設計與實現」之讀書筆記與整理 - 巢狀的 effect
· 2 min read
巢狀使用的情境
當 effectRegister 註冊內包了一個 effectRegister,就是所謂的巢狀
effectRegister(() => {
effectRegister(() => { /* ... */ })
/* ... */
})
在 Vue.js 內可以看作是父元件包著子元件,Foo 元件內要渲染 Bar 元件
// Bar 元件
const Bar = {
render() { /* ... */ },
}
// Foo 组件渲染了 Bar 元件
const Foo = {
render() {
return <Bar /> // jsx 語法
},
}
相當於
effectRegister(() => {
Foo.render()
// 巢狀
effectRegister(() => {
Bar.render()
})
})
巢狀導致收集到錯誤的 effect
在目前的實現中,若使用到巢狀的 effect,會有收集錯誤的問題,如下面這個範例:
effectRegister(() => {
console.log('effectRegister 1')
effectRegister(() => {
console.log('effectRegister 2')
document.body.innerText = proxy.age
})
document.body.innerText = proxy.text
})
setTimeout(() => {
proxy.text = '222'
}, 2000)
在 effectRegister 第一層打印 effectRegister 1,第二層打印 effectRegister 2,原本預期當 setTimeout 2 秒後,更改 proxy.text,會執行 effectRegister 第一層,印出 effectRegister 1,但結果不如預計,會印出 effectRegister 2:
'effectRegister 1'
'effectRegister 2'
'effectRegister 2'
這個問題的原因是源於 activeEffect 的設計,是單一個變數,所以當 effectRegister 第二層執行的時候, activeEffect 就被覆寫了,所以當 proxy.text 被 track 的時候,就會收集到第二層的 effectRegister。
/**
* 副作用函式
*/
let activeEffect
/**
* 註冊副作用的函式
* effect => effectRegister
*/
function effectRegister(fn) {
const effectFn = () => {
// 從桶中清除當前要執行的副作用
cleanup(effectFn)
// 寫入全域副作用變數,方便下次追蹤可以正常抓取
activeEffect = effectFn
// 執行副作用
fn()
}
// 初始化 effectFn.deps
effectFn.deps = []
effectFn()
}
重新設計 activeEffect
為了解決這個問題,我們會需要一個 stack 變數: activeEffectStack,在執行副作用函式前收集起來,stack 底部儲存的就是第一層副作用函式,stack 頂部儲存的就是內部副作用函數:

執行完 pop 出來,並切換 activeEffect 到前一個 effect。

依照 stack 的後進先出的設計,就可以解決巢狀執行帶來被覆寫的問題。
/**
* 副作用函式
*/
let activeEffect
const activeEffectStack = []
/**
* 註冊副作用的函式
* effect => effectRegister
*/
function effectRegister(fn) {
const effectFn = () => {
// 從桶中清除當前要執行的副作用
cleanup(effectFn)
// 寫入全域副作用變數,方便下次追蹤可以正常抓取
activeEffect = effectFn
activeEffectStack.push(effectFn)
// 執行副作用
fn()
activeEffectStack.pop()
activeEffect = activeEffectStack.at(-1)
}
// 初始化 effectFn.deps
effectFn.deps = []
effectFn()
}
完整程式碼
activeEffectStack - stackblitz
/**
* 副作用函式
*/
let activeEffect
const activeEffectStack = []
/**
* 註冊副作用的函式
* effect => effectRegister
*/
function effectRegister(fn) {
const effectFn = () => {
// 從桶中清除當前要執行的副作用
cleanup(effectFn)
// 寫入全域副作用變數,方便下次追蹤可以正常抓取
activeEffect = effectFn
activeEffectStack.push(effectFn)
// 執行副作用
fn()
activeEffectStack.pop()
activeEffect = activeEffectStack.at(-1)
}
// 初始化 effectFn.deps
effectFn.deps = []
effectFn()
}
function cleanup(effectFn) {
// 跑迴圈刪除,確保當前 effect 內收集的所有相同的副作用,只會執行一次
for (let i = 0; i < effectFn.deps.length; i++) {
const depsSet = effectFn.deps[i]
depsSet.forEach((i) => {})
depsSet.delete(effectFn)
}
// 清理 effectFn.deps
effectFn.deps.length = 0
}
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)
activeEffect.deps.push(deps)
}
// 在 set 函數内調用 trigger 函數觸發變化
function trigger(target, key) {
const effects = bucket.get(target)?.get(key)
const effectToRun = new Set(effects)
effectToRun && effectToRun.forEach(fn => fn())
}
effectRegister(() => {
console.log('effectRegister 1')
effectRegister(() => {
console.log('effectRegister 2')
document.body.innerText = proxy.age
})
document.body.innerText = proxy.text
})
setTimeout(() => {
proxy.text = '222'
}, 2000)