作為前端最熱門的三大框架/庫之一,Vue.js 讓我們能使用更易讀、更好維護的方式建構出頁面中的功能。不知道大家是否想過框架中所提供的 API 底層是如何運作的?這篇文就來記錄關於響應式的部分。
文章內容主要都來自 Vue.js 官方團隊成員霍春陽撰寫的 Vue.js 設計與實現並加上我自己的一點超譯,算是一份閱讀筆記。
上一章主要讓 effect
函式的邏輯不被寫死,並可以為鍵與副作用函式間建立關係,避免不同的鍵導致相同的副作用函式重複觸發。然而,目前仍有許多可以完善的空間。
為方便測試,可以先把目前的 effect
函式呼叫片段都刪除,接著為 originData
增加一個屬性,並建立一個新的 effect
函式呼叫:
const originData = { showText: true, text: "Hello Vue!" };
// ...
effect(function effectFn() {
document.getElementById("app").textContent = dataProxy.showText
? dataProxy.text
: "hiding text!";
});
此時建立的依賴關係會是這樣:
如果將 dataProxy.showText
改為 false
:
dataProxy.showText = false;
此時在 effectFn
的三元運算子中,dataProxy.text
會因為 dataProxy.showText
為 false
,而不會被讀取到。換句話說,當 showText
為 false
,理想的狀況會是:
目前的實現方式因為還做不到這點,就會導致 dataProxy.text
的副作用函式依然存在。假如在 dataProxy.showText
為 false
的情況下,dataProxy.text
的值發生了改變:
dataProxy.text = 'another text';
會導致 dataProxy.text
所依賴的副作用函式 effectFn
從 Set
撈出並執行。然而,因為 dataProxy.showText
為 false
,dataProxy.text
所依賴的副作用函式 effectFn
即使執行了也沒有任何意義。
要解決這個問題,就必須在副作用函式執行時,將其從所有有存入它的 Set
中移除。要做到這件事情,我們就必須知道哪些 Set
存入了它。首先我們需要調整 effect
函式及 track
函式:
// effect 函式
function effect(fn) {
const runningEffectFn = () => {
activeEffect = runningEffectFn;
fn();
};
runningEffectFn.dependencies = [];
runningEffectFn();
}// track 函式
function track(target, key) {
if (!activeEffect) return;
let targetDependenciesMap = effectsWeakMap.get(target);
if (!targetDependenciesMap) {
targetDependenciesMap = new Map();
effectsWeakMap.set(target, targetDependenciesMap);
}
let keyRelatedEffectsSet = targetDependenciesMap.get(key);
if (!keyRelatedEffectsSet) {
keyRelatedEffectsSet = new Set();
targetDependenciesMap.set(key, keyRelatedEffectsSet);
}
keyRelatedEffectsSet.add(activeEffect);
activeEffect.dependencies.push(keyRelatedEffectsSet);
}
當 effect
函式被呼叫時:
- 宣告一個
runningEffectFn
函式 - 將
runningEffectFn
的屬性dependencies
設為空陣列 - 呼叫
runningEffectFn
- 因為
runningEffectFn
呼叫,activeEffect
被賦予runningEffectFn
的參考,也就是說activeEffect
也有了為空陣列的dependencies
- 呼叫傳入的
fn
接著因為 fn
的執行:
document.getElementById("app").textContent = dataProxy.showText
? dataProxy.text
: "hiding text!";
讀取了 dataProxy.showText
,觸發了 track
函數,其最後一行:
activeEffect.dependencies.push(keyRelatedEffectsSet);
便會將 dataProxy.showText
對應的副作用函式集合存進 activeEffect.dependencies
中:
有了這層關係,我們就能在副作用函式執行時,透過 runningEffectFn.dependencies
取得所有的對應的副作用函式集合,並從副作用函式集合中將副作用函式刪除:
function effect(fn) {
const runningEffectFn = () => {
cleanup(runningEffectFn);
activeEffect = runningEffectFn;
fn();
};
runningEffectFn.dependencies = [];
runningEffectFn();
}
/**
* 將傳入的副作用函式從副作用函式集合中刪除,並清空傳入副作用函式存入的集合
* @param {function} runningEffectFn 要觸發的副作用函式
*/
function cleanup(runningEffectFn) {
for (let i = 0; i < runningEffectFn.dependencies.length; i++) {
const dependencies = runningEffectFn.dependencies[i];
dependencies.delete(runningEffectFn);
}
runningEffectFn.dependencies.length = 0;
}
當 cleanup
被呼叫時,會先將傳入的 runningEffectFn
中的 dependencies
,也就是副作用函式集合一一取出,並將裡面存的 runningEffectFn
給刪除,並在最後清空 runningEffectFn.dependencies
。
此時,如果嘗試去觸發 dataProxy
的 set
方法,例如(不建議照著做):
dataProxy.text = 'modified';
會導致無限循環,原因是因為 trigger
函式中:
const keyRelatedEffectsSet = targetDependenciesMap.get(key);
if (keyRelatedEffectsSet) {
keyRelatedEffectsSet.forEach((effectFn) => effectFn());
}
在這裡我們從 keyRelatedEffectsSet
取出了副作用函式並執行。當副作用函式執行時:
() => {
cleanup(runningEffectFn);
activeEffect = runningEffectFn;
fn();
}
cleanup
函式會從 runningEffectFn.dependencies
中從裡面的副作用函式集合中將 runningEffectFn
刪除,但是副作用函式的執行會導致副作用函式重新被加入副作用函式集合中。在集合中,如果在 forEach 期間刪除了內部元素隨後又增加回來,該元素就會重新被讀取:
// 會導致無窮迴圈,執行前請三思
const set = new Set(['effectFn'])
set.forEach(item => {
console.log('set foreach running')
set.delete('effectFn')
set.add('effectFn')
})
解決方式很簡單,就是建立另一個 Set
並執行它:
const set = new Set(['effectFn'])
const anotherSet = new Set(set)
anotherSet.forEach(item => {
console.log('set foreach running')
set.delete('effectFn')
set.add('effectFn')
})
同樣的用法可以直接在 trigger
中使用:
function trigger(target, key) {
const targetDependenciesMap = effectsWeakMap.get(target);
if (!targetDependenciesMap) return;
const keyRelatedEffectsSet = targetDependenciesMap.get(key);
// 避免 forEach 進入無窮迴圈
const effectsToRun = new Set(keyRelatedEffectsSet);
effectsToRun.forEach((effectFn) => effectFn());
}
便解決了 forEach
問題。
以上便是解決當副作用函式執行時也沒有任何意義時依然會執行問題的處理方式,目前的響應式依然有完善的空間,下篇會繼續記錄。
References: