Vue.js 3 學習筆記-透過實現的方式理解 Vue.js 的響應式原理(三)

Eason Lin
10 min readJul 6, 2022

--

Photo by Boris Smokrovic on Unsplash

作為前端最熱門的三大框架/庫之一,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.showTextfalse,而不會被讀取到。換句話說,當 showTextfalse,理想的狀況會是:

因為 dataProxy.text 在此情況不會被讀取到,理想狀況是連它對應的副作用函式也不保留

目前的實現方式因為還做不到這點,就會導致 dataProxy.text 的副作用函式依然存在。假如在 dataProxy.showTextfalse 的情況下,dataProxy.text 的值發生了改變:

dataProxy.text = 'another text';

會導致 dataProxy.text 所依賴的副作用函式 effectFnSet 撈出並執行。然而,因為 dataProxy.showTextfalsedataProxy.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

此時,如果嘗試去觸發 dataProxyset 方法,例如(不建議照著做):

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:

--

--

Eason Lin
Eason Lin

Written by Eason Lin

Frontend Web Developer | Books

No responses yet