「Vue 設計與實現」響應系統原理(五)- 巢狀的 effect
「Vue.js 設計與實現」之讀書筆記與整理 - 巢狀的 effect
-9 min read關聯: [[「Vue 設計與實現」第二篇 - 響應系統的作用與實現]]
巢狀使用的情境
當 effectRegister 註冊內包了一個 effectRegister,就是所謂的巢狀
nested.js
effectRegister(() => { effectRegister(() => { /* ... */ }) /* ... */})
在 Vue.js 內可以看作是父元件包著子元件,Foo 元件內要渲染 Bar 元件
nested-vue.js
// Bar 元件const Bar = { render() { /* ... */ },}// Foo 组件渲染了 Bar 元件const Foo = { render() { return <Bar /> // jsx 語法 },}
相當於
nested-vue.js
effectRegister(() => { Foo.render() // 巢狀 effectRegister(() => { Bar.render() })})
巢狀導致收集到錯誤的 effect
在目前的實現中,若使用到巢狀的 effect,會有收集錯誤的問題,如下面這個範例:
effect.js
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 activeEffectconst 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
effect-stack.js
/** * 副作用函式 */let activeEffectconst 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)
Table of Contents