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

Eason Lin
8 min readJul 2, 2022

--

Photo by Juan Davila on Unsplash

作為前端最熱門的三大框架/庫之一,Vue.js 讓我們能使用更易讀、更好維護的方式建構出頁面中的功能。不知道大家是否想過框架中所提供的 API 底層是如何運作的?這篇文就來記錄關於響應式的部分。

文章內容主要都來自 Vue.js 官方團隊成員霍春陽撰寫的 Vue.js 設計與實現並加上我自己的一點超譯,算是一份閱讀筆記。

相信學習過 Vue.js 3 的開發者都知道,Vue.js 3 的響應式主要是採用 Proxy 來實現。Proxy 可以讓我們為物件自定操作行為,因此像是:

<div id="app">{{ message }}</div><script>
const { createApp } = Vue;
createApp({
data() {
return {
message: "Hello Vue!",
};
},
mounted() {
setTimeout(() => {
this.message = "Text is changed!";
}, 2000);
},
}).mount("#app");
</script>

僅僅透過

this.message = “Text is changed!”;

就能將 DOM 的文字直接更新,而非去呼叫並修改 div#apptextContent ,背後都是有 Proxy 在默默地工作著。那 Proxy 究竟在背後做了什麼事情,就從實現一個類似概念的響應式功能開始。

首先,我們定義出一個儲存原始資料的物件 originalData 及代理它進行作業的 dataProxy

const originData = { text: "Hello Vue!" };
const dataProxy = new Proxy(originData, {
get(target, key) {
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
return true;
},
});
console.log(dataProxy.text); // hello world
dataProxy.text = "Text is changed!";
console.log(originData.text); // Text is changed!

當嘗試去讀取 dataProxy 鍵的值時,dataProxy 會透過 get 方法直接回傳代理對象 originData 的鍵並回傳對應的值;而當去設定它鍵的值時,則會透過 set 方法將 originData 對應鍵的值更新。到目前為止都類似於直接操作 originData

那如何實現一個最陽春的響應式功能呢?我們可以在 Proxy 被讀取或設定時為其加上副作用函式

副作用

許多框架/庫,像是 React.js 官方文件中也不乏多次提及副作用這個詞。那副作用代表什麼意思呢?假設我有一個函式:

function effect() {
document.getElementById("app").textContent = "Hey man!";
}
effect();

這個函式會直接將 div#app 內的值複寫成 Hey man!,倘若有其他函式會去讀取 div#app 並拿來用於其他用途,那麼可以說這兩個函式具有副作用。

這就有點類似我們吃藥時,藥單上通常會撰寫每顆藥丸的副作用,例如緩解腹瀉的藥,副作用可能是嗜睡及頭暈,明明主要的功能是緩解腹瀉,實際上這顆藥丸卻做了其他事情讓患者感到嗜睡及頭暈。

有個很有意思的例子:

當使用 === 時,比較 abbc 皆會回傳 false,然而使用 == 比較 ac 時卻回傳了 true,這正是因為 == 本身會產生副作用,詳細的原因可參考重新認識 JavaScript 番外篇 (7) — 判斷式 (a == 1 && a == 2 && a == 3) 結果為 true ?,少用 == 可以避免掉一些奇怪的問題。

當我們說此函式具有副作用,那麼代表此函式的執行會影響到其他函式的執行(這句話幾乎是從書上抄來,但我想不到更好的說法了)

實現簡易的響應式

接著我們就可以試著實現一個最陽春的響應式功能。我們在一開始已經定義好了原始資料 originalData 及它的代理 dataProxy,接著我們便能在 dataProxy 的 getset 時進行操作。

首先,響應式會起始於副作用函式的執行,因為沒有副作用函式的執行,即使資料被讀取或改變了,程式也無法預測該做什麼事情:

function effect() {
document.getElementById("app").textContent = dataProxy.text;
}
effect();

effect 執行後,因為 dataProxy.text 的讀取,dataProxy 中的 get 方法會被觸發,由於我們在 get 中回傳了 target[key]div#app 會直接渲染出 originData.text 的值:Hello Vue!

到目前為止都很正常。但如果 dataProxy.text 被修改了,我們要如何響應式地更新 div#app 的內容呢?

我們可以使用一個集合(Set),就稱呼它為 effectsSet。為何會使用 Set 作為儲存 effect 的資料結構,底下會有說明。在 dataProxy.text 被讀取時將其存入 effectSets 中:

const effectsSet = new Set();
const dataProxy = new Proxy(originData, {
get(target, key) {
effectsSet.add(effect);
return target[key];
},

如果用圖片描述就類似:

那如何在 dataProxy.text 被修改時更新 div#app 的值呢?當 dataProxy.text 被賦值時,會觸發 dataProxyset 方法,而我們在稍早已經將副作用函式存入了 effectsSet 集合中,接下來我們只要在 set 方法被執行時,於值更新後將 effectoffsetsSet 抓出來執行即可:

set(target, key, newVal) {
target[key] = newVal;
effectsSet.forEach((fn) => fn());
return true;
}

接著我們試試看在 effect 執行後更新 dataProxy.text 看看:

function effect() {
console.log("effect run!");
document.getElementById("app").textContent = dataProxy.text;
}
effect();
setTimeout(() => {
dataProxy.text = "Text is changed!";
}, 2000);

如果用圖片描述就類似:

到這裡時,由於 effect 又被執行了一次,dataProxyget 會再次被觸發,而 effectsSet 由於是 Set具有僅會存入一個相同物件參考的特性,即使 effectsSet.add(effect) 被執行了,effect 也不會被存入 efffectsSet 中,便能避免重複存入導致後續重複呼叫的窘境。

以上便是實現最簡易響應式的記錄,說它簡易,是因為有太多的狀況還等待著處理,距離完整的響應式還有一定的程式碼需要追加,後續會在記錄更多。如果內文有任何錯誤,還請不吝指出,謝謝大家。

References:

https://ithelp.ithome.com.tw/articles/10197606

--

--