作為前端最熱門的三大框架/庫之一,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#app
的 textContent
,背後都是有 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
並拿來用於其他用途,那麼可以說這兩個函式具有副作用。
這就有點類似我們吃藥時,藥單上通常會撰寫每顆藥丸的副作用,例如緩解腹瀉的藥,副作用可能是嗜睡及頭暈,明明主要的功能是緩解腹瀉,實際上這顆藥丸卻做了其他事情讓患者感到嗜睡及頭暈。
有個很有意思的例子:
當使用 ===
時,比較 a
與 b
,b
與 c
皆會回傳 false
,然而使用 ==
比較 a
與 c
時卻回傳了 true
,這正是因為 ==
本身會產生副作用,詳細的原因可參考重新認識 JavaScript 番外篇 (7) — 判斷式 (a == 1 && a == 2 && a == 3) 結果為 true ?,少用 ==
可以避免掉一些奇怪的問題。
當我們說此函式具有副作用,那麼代表此函式的執行會影響到其他函式的執行(這句話幾乎是從書上抄來,但我想不到更好的說法了)
實現簡易的響應式
接著我們就可以試著實現一個最陽春的響應式功能。我們在一開始已經定義好了原始資料 originalData
及它的代理 dataProxy
,接著我們便能在 dataProxy 的 get
或 set
時進行操作。
首先,響應式會起始於副作用函式的執行,因為沒有副作用函式的執行,即使資料被讀取或改變了,程式也無法預測該做什麼事情:
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
被賦值時,會觸發 dataProxy
的 set
方法,而我們在稍早已經將副作用函式存入了 effectsSet
集合中,接下來我們只要在 set
方法被執行時,於值更新後將 effect
從 offsetsSet
抓出來執行即可:
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
又被執行了一次,dataProxy
的 get
會再次被觸發,而 effectsSet
由於是 Set
,具有僅會存入一個相同物件參考的特性,即使 effectsSet.add(effect)
被執行了,effect
也不會被存入 efffectsSet
中,便能避免重複存入導致後續重複呼叫的窘境。
以上便是實現最簡易響應式的記錄,說它簡易,是因為有太多的狀況還等待著處理,距離完整的響應式還有一定的程式碼需要追加,後續會在記錄更多。如果內文有任何錯誤,還請不吝指出,謝謝大家。
References: