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
動(dòng)作觸發(fā)副作用函數(shù)重新執(zhí)行時(shí),有能力決定副作用函數(shù)執(zhí)行的時(shí)機(jī)、次數(shù)以及方式。
有了調(diào)度函數(shù),我們?cè)?code>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ù)對(duì)其執(zhí)行兩次自增操作,輸出如下:
1
2
3
由輸出結(jié)果可知,obj.foo
的值一定會(huì)從1自增到3,2只是它的過渡狀態(tài)。如果我們只關(guān)心最終結(jié)果而不關(guān)心過程,那么執(zhí)行三次打印操作是多余的,我們期望的打印結(jié)果是:
1
3
那么就考慮傳入調(diào)度器函數(shù)去幫助我們實(shí)現(xiàn)此功能,那由此需求,我們先來實(shí)現(xiàn)一下scheduler功能。
二、單元測(cè)試
首先還是藉由單測(cè)來梳理一下功能,這是直接從vue3
源碼中粘貼過來對(duì)scheduler的
單測(cè),里面很詳細(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è)單測(cè)的流程:
- 通過
effect
的第二個(gè)參數(shù)給定的一個(gè)對(duì)象{ scheduler: () => {} }
, 屬性是scheduler
, 值是一個(gè)函數(shù); effect
第一次執(zhí)行的時(shí)候, 還是會(huì)執(zhí)行fn
;- 當(dāng)響應(yīng)式對(duì)象被
set
,也就是數(shù)據(jù)update
時(shí), 如果scheduler
存在, 則不會(huì)執(zhí)行fn
, 而是執(zhí)行scheduler
; - 當(dāng)再次執(zhí)行
runner
的時(shí)候, 才會(huì)再次的執(zhí)行fn
.
三、代碼實(shí)現(xiàn)
那接下來就直接開始代碼實(shí)現(xiàn)功能,這里直接貼出完整代碼了,// + 會(huì)標(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)完成,那接下來看一下單測(cè)結(jié)果。
四、回歸實(shí)現(xiàn)
好,現(xiàn)在我們?cè)倩氐阶畛醯睦踝??,在上面scheduler
基礎(chǔ)上,完成現(xiàn)有需求,繼續(xù)看一下對(duì)此需求的單測(cè)。
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ù)組存儲(chǔ)后進(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(); } }
再觀察上面的單測(cè)代碼,首先,我們定義了一個(gè)任務(wù)隊(duì)列jobQueue
,它是一個(gè)Set
數(shù)據(jù)結(jié)構(gòu),目的是利用Set
數(shù)據(jù)結(jié)構(gòu)的自動(dòng)去重功能。
接著我們看調(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)志就會(huì)設(shè)置為true,意思是無論調(diào)用多少次flushJob
函數(shù),在一個(gè)周期內(nèi)都只會(huì)執(zhí)行一次。
需要注意的是,在flushJob
內(nèi)通過p.then
將一個(gè)函數(shù)添加到微任務(wù)隊(duì)列
,在微任務(wù)隊(duì)列
內(nèi)完成對(duì)jobQueue
的遍歷執(zhí)行。
整段代碼的效果是,連續(xù)對(duì)obj.foo
執(zhí)行兩次自增操作,會(huì)同步且連續(xù)地執(zhí)行兩次scheduler
調(diào)度函數(shù),這意味著同一個(gè)副作用函數(shù)會(huì)被jobQueue.add(fn)
添加兩次,但由于Set
數(shù)據(jù)結(jié)構(gòu)的去重能力,最終jobQueue
中只會(huì)有一項(xiàng),即當(dāng)前副作用函數(shù)。
類似地,flushJob
也會(huì)同步且連續(xù)執(zhí)行兩次,但由于isFlushing
標(biāo)志的存在,實(shí)際上flushJob
函數(shù)在一個(gè)事件循環(huán)內(nèi)只會(huì)執(zhí)行一次,即在微任務(wù)隊(duì)列內(nèi)執(zhí)行一次。
當(dāng)微任務(wù)隊(duì)列開始執(zhí)行時(shí),就會(huì)遍歷jobQueue
并執(zhí)行里面存儲(chǔ)的副作用函數(shù)。由于此時(shí)jobQueue
隊(duì)列內(nèi)只有一個(gè)副作用函數(shù),所以只會(huì)執(zhí)行一次,并且當(dāng)它執(zhí)行時(shí),字段obj.foo
的值已經(jīng)是3了,這樣我們就實(shí)現(xiàn)了期望的輸出。
再跑一遍完整流程,來看一下單測(cè)結(jié)果,確保新增代碼不影響以往功能。
測(cè)試結(jié)束完以后,由于job queue
是一個(gè)實(shí)際案例單測(cè),所以我們將其抽離到examples
下面的testCase
里,建立jobQueue.spec.ts
。
五、結(jié)語(yǔ)
可能你已經(jīng)注意到了,這個(gè)功能點(diǎn)類似于在Vue.js
中連續(xù)多次修改響應(yīng)式數(shù)據(jù)但只會(huì)觸發(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的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue利用Blob下載原生二進(jìn)制數(shù)組文件
這篇文章主要為大家詳細(xì)介紹了Vue利用Blob下載原生二進(jìn)制數(shù)組文件,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-09-09vue框架編輯接口頁(yè)面下拉級(jí)聯(lián)選擇并綁定接口所屬模塊
這篇文章主要為大家介紹了vue框架編輯接口頁(yè)面實(shí)現(xiàn)下拉級(jí)聯(lián)選擇以及綁定接口所屬模塊,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05vue報(bào)錯(cuò)Failed to execute 'appendChild&apos
這篇文章主要為大家介紹了vue報(bào)錯(cuò)Failed to execute 'appendChild' on 'Node'解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11vue.js通過路由實(shí)現(xiàn)經(jīng)典的三欄布局實(shí)例代碼
本文通過實(shí)例代碼給大家介紹了vue.js通過路由實(shí)現(xiàn)經(jīng)典的三欄布局,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2018-07-07Vue CLI4 Vue.config.js標(biāo)準(zhǔn)配置(最全注釋)
這篇文章主要介紹了Vue CLI4 Vue.config.js標(biāo)準(zhǔn)配置,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06vue采用EventBus實(shí)現(xiàn)跨組件通信及注意事項(xiàng)小結(jié)
EventBus是一種發(fā)布/訂閱事件設(shè)計(jì)模式的實(shí)踐。這篇文章主要介紹了vue采用EventBus實(shí)現(xiàn)跨組件通信及注意事項(xiàng),需要的朋友可以參考下2018-06-06vue前端如何接收后端傳過來的帶list集合的數(shù)據(jù)
這篇文章主要介紹了vue前端如何接收后端傳過來的帶list集合的數(shù)據(jù),前后端交互,文中的示例Json報(bào)文,前端采用vue進(jìn)行接收,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-02-02