vue3調(diào)度器effect的scheduler功能實(shí)現(xiàn)詳解
一、調(diào)度執(zhí)行
說到scheduler,也就是vue3的調(diào)度器,可能大家還不是特別明白調(diào)度器的是什么,先大概介紹一下。
可調(diào)度性是響應(yīng)式系統(tǒng)非常重要的特性。首先我們要明確什么是可調(diào)度性。所謂可調(diào)度性,指的是當(dāng)trigger 動作觸發(fā)副作用函數(shù)重新執(zhí)行時(shí),有能力決定副作用函數(shù)執(zhí)行的時(shí)機(jī)、次數(shù)以及方式。
有了調(diào)度函數(shù),我們在trigger函數(shù)中觸發(fā)副作用函數(shù)重新執(zhí)行時(shí),就可以直接調(diào)用用戶傳遞的調(diào)度器函數(shù),從而把控制權(quán)交給用戶。
舉個(gè)栗子??:
const obj = reactive({ foo: 1 });
effect(() => {
console.log(obj.foo);
})
obj.foo++;
obj.foo++;
首先在副作用函數(shù)中打印obj.foo的值,接著連續(xù)對其執(zhí)行兩次自增操作,輸出如下:
1
2
3
由輸出結(jié)果可知,obj.foo的值一定會從1自增到3,2只是它的過渡狀態(tài)。如果我們只關(guān)心最終結(jié)果而不關(guān)心過程,那么執(zhí)行三次打印操作是多余的,我們期望的打印結(jié)果是:
1
3
那么就考慮傳入調(diào)度器函數(shù)去幫助我們實(shí)現(xiàn)此功能,那由此需求,我們先來實(shí)現(xiàn)一下scheduler功能。
二、單元測試
首先還是藉由單測來梳理一下功能,這是直接從vue3源碼中粘貼過來對scheduler的單測,里面很詳細(xì)的描述了scheduler的功能。
it('scheduler', () => {
let dummy;
let run: any;
const scheduler = jest.fn(() => {
run = runner;
});
const obj = reactive({ foo: 1 });
const runner = effect(
() => {
dummy = obj.foo;
},
{ scheduler },
);
expect(scheduler).not.toHaveBeenCalled();
expect(dummy).toBe(1);
// should be called on first trigger
obj.foo++;
expect(scheduler).toHaveBeenCalledTimes(1);
// should not run yet
expect(dummy).toBe(1);
// manually run
run();
// should have run
expect(dummy).toBe(2);
});
大概介紹一下這個(gè)單測的流程:
- 通過
effect的第二個(gè)參數(shù)給定的一個(gè)對象{ scheduler: () => {} }, 屬性是scheduler, 值是一個(gè)函數(shù); effect第一次執(zhí)行的時(shí)候, 還是會執(zhí)行fn;- 當(dāng)響應(yīng)式對象被
set,也就是數(shù)據(jù)update時(shí), 如果scheduler存在, 則不會執(zhí)行fn, 而是執(zhí)行scheduler; - 當(dāng)再次執(zhí)行
runner的時(shí)候, 才會再次的執(zhí)行fn.
三、代碼實(shí)現(xiàn)
那接下來就直接開始代碼實(shí)現(xiàn)功能,這里直接貼出完整代碼了,// + 會標(biāo)注出新增加的代碼。
class ReactiveEffect {
private _fn: any;
// + 接收scheduler
// + 在構(gòu)造函數(shù)的參數(shù)上使用public等同于創(chuàng)建了同名的成員變量
constructor(fn, public scheduler?) {
this._fn = fn;
}
run() {
activeEffect = this;
return this._fn();
}
}
// * ============================== ↓ 依賴收集 track ↓ ============================== * //
// * targetMap: target -> key
const targetMap = new WeakMap();
// * target -> key -> dep
export function track(target, key) {
// * depsMap: key -> dep
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
// * dep
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(activeEffect);
}
// * ============================== ↓ 觸發(fā)依賴 trigger ↓ ============================== * //
export function trigger(target, key) {
let depsMap = targetMap.get(target);
let dep = depsMap.get(key);
for (const effect of dep) {
// + 判斷是否有scheduler, 有則執(zhí)行,無則執(zhí)行fn
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
let activeEffect;
export function effect(fn, options: any = {}) {
// + 直接將scheduler掛載到依賴上
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
return _effect.run.bind(_effect);
}
代碼實(shí)現(xiàn)完成,那接下來看一下單測結(jié)果。

四、回歸實(shí)現(xiàn)
好,現(xiàn)在我們再回到最初的栗子??,在上面scheduler基礎(chǔ)上,完成現(xiàn)有需求,繼續(xù)看一下對此需求的單測。
it('job queue', () => {
// 定義一個(gè)任務(wù)隊(duì)列
const jobQueue = new Set();
// 使用 Promise.resolve() 創(chuàng)建一個(gè) Promise 實(shí)例,我們用它將一個(gè)任務(wù)添加到微任務(wù)隊(duì)列
const p = Promise.resolve();
// 一個(gè)標(biāo)志代表是否正在刷新隊(duì)列
let isFlushing = false;
function flushJob() {
// 如果隊(duì)列正在刷新,則什么都不做
if (isFlushing) return;
// 設(shè)置為true,代表正在刷新
isFlushing = true;
// 在微任務(wù)隊(duì)列中刷新 jobQueue 隊(duì)列
p.then(() => {
jobQueue.forEach((job: any) => job());
}).finally(() => {
// 結(jié)束后重置 isFlushing
isFlushing = false;
// 雖然scheduler執(zhí)行兩次,但是由于是Set,所以只有一項(xiàng)
expect(jobQueue.size).toBe(1);
// 期望最終結(jié)果拿數(shù)組存儲后進(jìn)行斷言
expect(logArr).toEqual([1, 3]);
});
}
const obj = reactive({ foo: 1 });
let logArr: number[] = [];
effect(
() => {
logArr.push(obj.foo);
},
{
scheduler(fn) {
// 每次調(diào)度時(shí),將副作用函數(shù)添加到 jobQueue 隊(duì)列中
jobQueue.add(fn);
// 調(diào)用 flushJob 刷新隊(duì)列
flushJob();
},
},
);
obj.foo++;
obj.foo++;
expect(obj.foo).toBe(3);
});
在分析上段代碼之前,為了輔助完成上述功能,我們需要回到trigger中,調(diào)整一下遍歷執(zhí)行,為了讓我們的scheduler能拿到原始依賴。
for (const effect of dep) {
// + 判斷是否有scheduler, 有則執(zhí)行,無則執(zhí)行fn
if (effect.scheduler) {
effect.scheduler(effect._fn);
} else {
effect.run();
}
}
再觀察上面的單測代碼,首先,我們定義了一個(gè)任務(wù)隊(duì)列jobQueue,它是一個(gè)Set數(shù)據(jù)結(jié)構(gòu),目的是利用Set數(shù)據(jù)結(jié)構(gòu)的自動去重功能。
接著我們看調(diào)度器scheduler的實(shí)現(xiàn),在每次調(diào)度執(zhí)行時(shí),先將當(dāng)前副作用函數(shù)添加到jobQueue隊(duì)列中,再調(diào)用flushJob函數(shù)刷新隊(duì)列。
然后我們把目光轉(zhuǎn)向flushJob函數(shù),該函數(shù)通過isFlushing標(biāo)志判斷是否需要執(zhí)行,只有當(dāng)其為false 時(shí)才需要執(zhí)行,而一旦flushJob函數(shù)開始執(zhí)行,isFlushing標(biāo)志就會設(shè)置為true,意思是無論調(diào)用多少次flushJob函數(shù),在一個(gè)周期內(nèi)都只會執(zhí)行一次。
需要注意的是,在flushJob內(nèi)通過p.then將一個(gè)函數(shù)添加到微任務(wù)隊(duì)列,在微任務(wù)隊(duì)列內(nèi)完成對jobQueue的遍歷執(zhí)行。
整段代碼的效果是,連續(xù)對obj.foo執(zhí)行兩次自增操作,會同步且連續(xù)地執(zhí)行兩次scheduler調(diào)度函數(shù),這意味著同一個(gè)副作用函數(shù)會被jobQueue.add(fn)添加兩次,但由于Set數(shù)據(jù)結(jié)構(gòu)的去重能力,最終jobQueue中只會有一項(xiàng),即當(dāng)前副作用函數(shù)。
類似地,flushJob也會同步且連續(xù)執(zhí)行兩次,但由于isFlushing標(biāo)志的存在,實(shí)際上flushJob函數(shù)在一個(gè)事件循環(huán)內(nèi)只會執(zhí)行一次,即在微任務(wù)隊(duì)列內(nèi)執(zhí)行一次。
當(dāng)微任務(wù)隊(duì)列開始執(zhí)行時(shí),就會遍歷jobQueue并執(zhí)行里面存儲的副作用函數(shù)。由于此時(shí)jobQueue隊(duì)列內(nèi)只有一個(gè)副作用函數(shù),所以只會執(zhí)行一次,并且當(dāng)它執(zhí)行時(shí),字段obj.foo的值已經(jīng)是3了,這樣我們就實(shí)現(xiàn)了期望的輸出。
再跑一遍完整流程,來看一下單測結(jié)果,確保新增代碼不影響以往功能。

測試結(jié)束完以后,由于job queue是一個(gè)實(shí)際案例單測,所以我們將其抽離到examples下面的testCase里,建立jobQueue.spec.ts。
五、結(jié)語
可能你已經(jīng)注意到了,這個(gè)功能點(diǎn)類似于在Vue.js中連續(xù)多次修改響應(yīng)式數(shù)據(jù)但只會觸發(fā)一次更新,實(shí)際上Vue.js內(nèi)部實(shí)現(xiàn)了一個(gè)更加完善的調(diào)度器,思路與上文介紹的相同。
此外,綜合前面的這些內(nèi)容,我們就可以實(shí)現(xiàn)Vue.js中一個(gè)非常重要且非常有特色的能力:computed計(jì)算屬性,這個(gè)就后面再慢慢實(shí)現(xiàn)吧...
以上就是vue3調(diào)度器effect的scheduler功能實(shí)現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于vue3調(diào)度器effect scheduler的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue利用Blob下載原生二進(jìn)制數(shù)組文件
這篇文章主要為大家詳細(xì)介紹了Vue利用Blob下載原生二進(jìn)制數(shù)組文件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09
vue框架編輯接口頁面下拉級聯(lián)選擇并綁定接口所屬模塊
這篇文章主要為大家介紹了vue框架編輯接口頁面實(shí)現(xiàn)下拉級聯(lián)選擇以及綁定接口所屬模塊,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05
vue報(bào)錯(cuò)Failed to execute 'appendChild&apos
這篇文章主要為大家介紹了vue報(bào)錯(cuò)Failed to execute 'appendChild' on 'Node'解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
vue.js通過路由實(shí)現(xiàn)經(jīng)典的三欄布局實(shí)例代碼
本文通過實(shí)例代碼給大家介紹了vue.js通過路由實(shí)現(xiàn)經(jīng)典的三欄布局,代碼簡單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2018-07-07
Vue CLI4 Vue.config.js標(biāo)準(zhǔn)配置(最全注釋)
這篇文章主要介紹了Vue CLI4 Vue.config.js標(biāo)準(zhǔn)配置,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
vue采用EventBus實(shí)現(xiàn)跨組件通信及注意事項(xiàng)小結(jié)
EventBus是一種發(fā)布/訂閱事件設(shè)計(jì)模式的實(shí)踐。這篇文章主要介紹了vue采用EventBus實(shí)現(xiàn)跨組件通信及注意事項(xiàng),需要的朋友可以參考下2018-06-06
vue前端如何接收后端傳過來的帶list集合的數(shù)據(jù)
這篇文章主要介紹了vue前端如何接收后端傳過來的帶list集合的數(shù)據(jù),前后端交互,文中的示例Json報(bào)文,前端采用vue進(jìn)行接收,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-02-02

