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

Eason Lin
9 min readJul 8, 2022

--

Photo by Brad West on Unsplash

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

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

在我們觸發副作用函式重新執行時,可能會希望可以控制其執行的時機、次數或方式。例如以下的片段:

effect(() => {
console.log(dataProxy.text);
});
dataProxy.text = "text updated";console.log("end");

Console 中印出的結果會是:

假如我們希望能在不改變程式碼的情況下將 text updatedend 印出的順序調換,就必須讓響應式能做到調度(schedule)

調度器(scheduler)

我們可以為 effect 增加第二個物件參數 options

/**
* 用於註冊及觸發副作用函式
* @param {function} fn 要觸發的副作用函式
* @param {object} options 客製化選項
* @returns {void}
*/
function effect(fn, options = {}) {
const runningEffectFn = () => {
cleanup(runningEffectFn);
activeEffect = runningEffectFn;
effectStack.push(runningEffectFn);
fn();
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
};
runningEffectFn.options = options;
runningEffectFn.dependencies = [];
runningEffectFn();
}

除了 dependencies 外,我們也將 options 加進 runningEffectFn 上。在呼叫 effect 時,可以為 effect 增加第二個參數 options,裡面目前僅有一個函式屬性:

effect(
() => {
console.log(dataProxy.text);
},
{ scheduler(fn) {} }
);

接著調整 trigger 函式:

function trigger(target, key) {
const targetDependenciesMap = effectsWeakMap.get(target);
if (!targetDependenciesMap) return;
const keyRelatedEffectsSet = targetDependenciesMap.get(key);
const effectsToRun = new Set();
if (keyRelatedEffectsSet) {
keyRelatedEffectsSet.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn);
}
});
}
effectsToRun.forEach((effectFn) => {
if (typeof effectFn.options.scheduler === "function") {
effectFn.options.scheduler(effectFn);
} else {
effectFn();
}
});
}

我們在 effectsToRun.forEach 增加了 ifelse 的判斷式,else 保留了原本的執行方式,我們看看 if

if (typeof effectFn.options.scheduler === "function") {
effectFn.options.scheduler(effectFn);
}

這邊做的就是在副作用函式即將執行前做一次檢查,因為我們在 effect 函式呼叫時已經有將 options 寫入副作用函式:

runningEffectFn.options = options;

此時 effectFn.options 如果有存在 scheduler,應該會是一個 function

effect(
() => {
console.log(dataProxy.text);
},
{ scheduler(fn) {} } // 會對應到這個 scheduler
);

這時應該就能看出 fn 參數被傳入的會是即將執行的副作用函式,如此在呼叫 effect 時就能決定如何執行副作用函式。回到一開始的提及的需求,如果我們希望 text updatedend 之後才被印出,就要讓 fn 延後呼叫,實際的做法就是運用 setTimeout

effect(
() => {
console.log(dataProxy.text);
},
{
scheduler(fn) {
setTimeout(fn);
},
}
);

上面有提到除了決定執行時機外,也能控制執行次數,接下來我們看看如何決定控制次數。

控制執行

假設程式碼如下:

effect(() => {
console.log(dataProxy.text);
});
dataProxy.text += " updated";
dataProxy.text += " updated";

此時 Console 應該會印出:

這樣完全沒有問題。但如果我們不想要在頻繁更新時不斷觸發副作用函式,例如我們只希望印出 Hello Vue!Hello Vue! updated updated,可以怎麼做呢?

首先,我們需要建立一個任務佇列:

const jobQueue = new Set();

我們還需要一個用來將程式推遲到微任務執行用的Promise.resolve()

const promise = Promise.resolve();

接著,我們會需要一個類似於 Ajax 判定是否為等待中的變數,我們稱它為 isFlushing

let isFlushing = false;

接著就可以撰寫用來控制執行的 flushJob

const jobQueue = new Set();
const promise = Promise.resolve();
let isFlushing = false;
/**
* 在連續修改多次響應式資料時,只觸發一次更新
* @returns {void}
*/
function flushJob() {
if (isFlushing) return;
isFlushing = true;
promise
.then(() => {
jobQueue.forEach((job) => job());
})
.finally(() => {
isFlushing = false;
});
}

flushJob 函式中,我們會用 isFlushing 作為微任務是否執行完畢的判定變數,如果微任務仍未執行,flushJob 會直接 return 結束。

接著我們會在微任務佇列中遍歷並執行 jobQueue 集合中的副作用函式,並在結束後將 isFlushing 恢復為 false

effect 呼叫時,我們可以在 scheduler 使用它:

effect(
() => {
console.log(dataProxy.text);
},
{
scheduler(fn) {
jobQueue.add(fn);
flushJob();
},
}
);

當這段程式碼執行:

dataProxy.text += " updated";
dataProxy.text += " updated";

會觸發 dataProxy set 方法中的 trigger 函式,trigger 含是會將副作用函式作為參數呼叫 scheduler,此時因為 jobQueue 集合的去重特性,最終只會存入一次副作用函式,而 flushJob 雖然被呼叫兩次,但因為在第一次時 isFlushing 已經被設為 true,因此第二次便不會執行。

scheduler 的第二次呼叫完畢後,才進入到微任務的執行階段,此時 dataProxy.text 的值已經被更新為 Hello Vue! updated updated,因此 console 便會印出:

便解決了需要控制執行的問題。

關於響應式原理的實現就到此告一段落了,Vue 在實踐上有非常多值得學習的地方,今後翻閱時若有發現值得紀錄的也會一併紀錄。如果這篇文有任何錯誤,還煩請不吝指出,謝謝大家。

References:

--

--

Eason Lin
Eason Lin

Written by Eason Lin

Frontend Web Developer | Books

No responses yet