作為前端最熱門的三大框架/庫之一,Vue.js 讓我們能使用更易讀、更好維護的方式建構出頁面中的功能。不知道大家是否想過框架中所提供的 API 底層是如何運作的?這篇文就來記錄關於響應式的部分。
文章內容主要都來自 Vue.js 官方團隊成員霍春陽撰寫的 Vue.js 設計與實現並加上我自己的一點超譯,算是一份閱讀筆記。
在我們觸發副作用函式重新執行時,可能會希望可以控制其執行的時機、次數或方式。例如以下的片段:
effect(() => {
console.log(dataProxy.text);
});dataProxy.text = "text updated";console.log("end");
Console 中印出的結果會是:
假如我們希望能在不改變程式碼的情況下將 text updated
和 end
印出的順序調換,就必須讓響應式能做到調度(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
增加了 if
和 else
的判斷式,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 updated
在 end
之後才被印出,就要讓 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: