zone.js由入門到放棄之三——zone.js 源碼分析【setTimeout篇】

Delegate是個好東西,看看孫嘯達(dá) 同學(xué)對ZoneDelegate的介紹吧,這是他關(guān)于zone.js系列文章的第三篇~
zone.js系列往期文章
zone.js由入門到放棄之一——通過一場游戲認(rèn)識zone.js
zone.js由入門到放棄之二——zone.js API大練兵
zone.js源碼分析
接下來的全是干貨,從頭到尾,一干到底

一點(diǎn)前置:Zone 和 ZoneDelegate
在前文中,我們一直在回避講解Zone和ZoneDelegate之間的區(qū)別。尤其在上篇文章講API的時候,我甚至讓大家把這兩者當(dāng)成一回事。其實(shí)這兩者并不是完全相等的。單從Delegate這個單詞你也能看出,雖然Zone和ZoneDelegate的API很像,但是真正干活的是ZoneDelegate。我簡單節(jié)選幾段Zone的源碼,大家不難發(fā)現(xiàn),大多數(shù)Zone的API都直接或間接通過代理中相對應(yīng)的API完成的。
我把上篇文章講到的API和ZoneDelegate之間的調(diào)用關(guān)系簡單梳理了一下。下文在分析源碼的時候,會有大量Zone、ZomeDelegate、ZomeTask三者之間相互調(diào)用的場景,實(shí)在理不清的地方可以返回這里看下。

雖然ZoneDelegate實(shí)際承擔(dān)了大量的工作,但是Zone也不是甩手掌柜,啥活兒也不干。在我個人看來,Zone其實(shí)主要只負(fù)責(zé)兩件事:
維護(hù)Zone的上下文棧:我們知道Zone是個具有繼承關(guān)系的鏈?zhǔn)浇Y(jié)構(gòu)。zone.js在全局會維護(hù)一個Zone棧幀,每當(dāng)我們在某個Zone中執(zhí)行代碼時,Zone要負(fù)責(zé)將當(dāng)前的Zone上下文置于棧幀中;當(dāng)代碼執(zhí)行完畢,又要負(fù)責(zé)將Zone棧幀恢復(fù)回去。
Zone還負(fù)責(zé)ZoneTask的狀態(tài)切換。上文說過,Zone可以對宏任務(wù)、微任務(wù)、事件進(jìn)行管理。那么每個任務(wù)在Zone中處于何種階段、何種狀態(tài)也是由Zone負(fù)責(zé)的。Zone會在適當(dāng)時候調(diào)用ZoneTask的_transitionTo方法切換ZoneTask的狀態(tài)。
接下來會把zone.js對setTimeout的Patch過程進(jìn)行詳細(xì)的說明,為了方便理解,其中涉及的大量源碼都是我簡化之后。
第一階段:zone.js打包setTimeout
Patch第一站
zone.js提供一個靜態(tài)方法用于Patch我們常見的API,對setTimeout的Patch位于zone.js/lib/browser/browser.ts
下:其中這個patchTimer(global, set, clear, 'Timeout');
就是本次源碼分析的起點(diǎn)。
代碼傳送門
戰(zhàn)術(shù)式閹割patchTimer
雖然patchTimer是打包setTimeout的關(guān)鍵代碼,但是為了先理清框架,我先把一些當(dāng)下沒那么重要的代碼都省略掉。通過下面的代碼我們發(fā)現(xiàn),patchTimer中最核心的一句就是:
setNative = patchMethod(...)
setNative從命名上不難理解,其實(shí)就是用來保存原生的setTimeout。除了保存原生setTimeout之外,我們在下一節(jié)中一起看下patchMethod對setTimeout還做了什么。
代碼傳送門
只會甩鍋的patchMethod

下面是簡化后的代碼,不難發(fā)現(xiàn)patchMethod就做了兩件事:
將原生setTimeout方法保存起來,保存在windiw.__zone_symbol__setTimeout中
通過patchFn方法打包setTimeout,并替換原windiw.setTimeout
patchFn
這個函數(shù)看起來有點(diǎn)繁瑣,其實(shí)這是對函數(shù)柯里化的應(yīng)用,是一種高階函數(shù)。如果對這塊知識不了解的可以簡單理解為它就是一個返回函數(shù)的函數(shù)。patchFn
的執(zhí)行會返回一個打包后的setTimeout,而對patchFn
的定義來自于上一節(jié)的patchTimer
方法中。所以我說patchMethod
甩鍋,說好的要打包setTimeout方法,結(jié)果打包工具還得patchTimer
函數(shù)提供。
代碼傳送門
看看zone.js對setTimeout到底干了什么:

再回到patchTimer
方法中,patchTime在調(diào)用patchMethod的時候傳入了一個patchFn
方法。這個方法對setTimeout干了兩件事:
通過timer方法把真實(shí)回調(diào)包裝了一下,實(shí)際上就是想保留this指針
調(diào)用scheduleMacroTaskWithCurrentZone方法封裝出一個task 【重點(diǎn)】
看到這里是不是有點(diǎn)似曾相識的感覺,這個task會不會是ZoneTask?scheduleMacroTaskWithCurrentZone會不會和scheduleMacroTask有關(guān)系?
這里可以很負(fù)責(zé)的告訴你,兩個的問題的答案都是肯定的哈!至于scheduleMacroTaskWithCurrentZone的源碼分析,我們稍作調(diào)整再繼續(xù)分析。
代碼傳送門
第一階段小結(jié):
我把第一階段稱作為打包階段,此處一般都在應(yīng)用初始化的時候執(zhí)行的,zone.js正是利用這段時間對各式各樣的API進(jìn)行了Monkey Patch操作。截止目前為止,zone.js對setTimeout的Patch操作其實(shí)并沒有什么特別。最核心的函數(shù)是patchTimer,雖然在這個階段中,該函數(shù)大部分功能都被戰(zhàn)術(shù)性閹割了,但是它將setTimeout的原生實(shí)現(xiàn)替換成了patchFn
。從patchFn
的實(shí)現(xiàn)我們可以看出,每當(dāng)我們觸發(fā)window.setTimeout時,就會有一個名為task的任務(wù)被創(chuàng)建出來。上一遍文章說過,zone.js可以把諸多異步操作封裝成ZoneTask,然后就可以對每個異步任務(wù)的生命周期進(jìn)行監(jiān)控、跟蹤。看到這里,是不是大致有點(diǎn)輪廓了。
下面這個圖,是我根據(jù)zone.js第一階段的動作描述的,方便大家配合源碼進(jìn)行理解。

我看很多文章都說過zone.js的Patch過程如何殘暴,光聽別人說有什么意思,不如自己來看看
第二階段:觸發(fā)setTimeout
上一階段中,zone.js強(qiáng)勢hack了setTimeout,讓setTimeout被調(diào)用時創(chuàng)建一個task。接下來,我們看下,當(dāng)一個打包的setTimeout被調(diào)用后的流程。
創(chuàng)建Task
先填個坑,上一節(jié)我說scheduleMacroTaskWithCurrentZone和scheduleMacroTask有關(guān)系,此處以源碼為證哈:代碼傳送門
scheduleMacroTask
非常簡單,創(chuàng)建一個ZoneTask后帥鍋給scheduleTask
函數(shù)。
代碼傳送門
在這里,這個新建的ZoneTask非常重要,它除了一些初始化操作以外,有3個值得大家注意的地方(其它作用暫時不大的代碼已經(jīng)被省略):
scheduleFn是zoneTask調(diào)度的關(guān)鍵代碼,這里具體的代碼在
patchTimer
中。但在之前被我戰(zhàn)術(shù)性閹割了,后續(xù)用到的時候我再展開解釋。這里先記住,task有個scheduleFn方法,方法來自patchTimer
,請死記!ZoneTask有個invoke方法,該方法實(shí)際是對zone.runTask的調(diào)用。zone.runTask后面會介紹,但是這里是ZoneTask和Zone之間聯(lián)系的一個橋梁,請死記!
_transitionTo
是ZoneTask狀態(tài)切換函數(shù),Zone就是通過這個函數(shù)來改變Task的狀態(tài),并對Task實(shí)施跟蹤監(jiān)控的,還是 bi~~~ 請死記!
代碼傳送門
調(diào)度Task
Task創(chuàng)建后,Zone會通過代理執(zhí)行scheduleTask完成對Task的調(diào)度。Zone只在ZoneDelegate調(diào)度前后分別去修改一下Task的狀態(tài)而已,真的是干啥全憑一張嘴。

代碼傳送門
ZoneDelegate.scheduleTask主要工作:
上篇文章中我們講到的onScheduleTask這個勾子會在此時被調(diào)用,這是zone.js跟蹤異步任務(wù)時觸發(fā)的第一個勾子。代碼中
this._scheduleTaskZS.onScheduleTask
的執(zhí)行就是這塊的體現(xiàn)。由于Zone有著一層層的繼承關(guān)系,所以源碼中其實(shí)還有很多父級代理中onScheduleTask勾子的調(diào)用邏輯。我為了方便理解,在下面代碼中把這部分代碼省略了,實(shí)際上scheduleTask這個方法會在這個過程中被遞歸調(diào)用多次。調(diào)度的核心是調(diào)用了task.scheduleFn方法,在上文中,我說這里是重點(diǎn),要死記的。
代碼傳送門
scheduleTask
函數(shù)的代碼不多,但是要了解它需要前面很多的鋪墊:
setNative方法被調(diào)用,前面講了這個方法是原生的setTimeout,也就是說執(zhí)行到這里,真正的setTimeout方法才剛被調(diào)用。
setTimeout的回調(diào)被重新封裝,封裝以后變成了task.invoke。從這一刻,zone.js正式改寫了setTimeout的回調(diào),并開始正式接管setTimeout。
task.invoke這個方法之前強(qiáng)調(diào)了要死記的,因?yàn)樗鼤g接調(diào)用zone.runTask方法。通過這樣的辦法,zone.js可以將setTimeout的回調(diào)方法限定在Zone的上下文中執(zhí)行。別看這里只有幾行,這是zone跨調(diào)用棧維持上下文統(tǒng)一的核心所在!
代碼傳送門
第二階段小結(jié):
對接上一階段,當(dāng)setTimeout被觸發(fā)后,zone會根據(jù)patch后的setTimeout新建一個Task(MacroTask)。這個task有個三個重要知識點(diǎn):
保存了該task的調(diào)度方法
scheduleFn
定義task的invoke方法
存在一個切換task狀態(tài)的方法
_transitionTo
。
接下來Zone把調(diào)度Task的工作承包給高啟強(qiáng),啊不對不對,是承包給ZoneDelegate,然后ZoneDelegate通過調(diào)用ZoneTask中scheduleFn完成任務(wù)調(diào)度。
scheduleFn
這個函數(shù)實(shí)際上hack掉了原生setTimeout方法上的回調(diào)函數(shù),將回調(diào)函數(shù)改寫成task的invoke方法。到此形成一個邏輯上的閉環(huán),一句話總結(jié):setTimeout的回調(diào)實(shí)際調(diào)用的是task.invoke函數(shù)。
下圖是到目前為止的調(diào)用關(guān)系圖:

第三階段:回調(diào)執(zhí)行
由于原生的setTimeout被觸發(fā),所以改寫后的回調(diào)被送進(jìn)循環(huán)隊列的Timer隊列中,待計時器計算延時到達(dá)后,將改寫后的回調(diào)放入執(zhí)行隊列等待執(zhí)行。這部分內(nèi)容是V8引擎的循環(huán)隊列的知識,這里就不展開講了。我們最關(guān)心的是,當(dāng)執(zhí)行棧開始執(zhí)行這個回調(diào)的時候又會發(fā)生什么?

Task運(yùn)行
回調(diào)函數(shù)執(zhí)行的時候,實(shí)際執(zhí)行的是task.invkoe方法;又由于task.invkoe綁定Zone.runTask。當(dāng)然,一看到Zone上方法,那我們可以毫無波瀾地判斷,此時Zone除了改一改Task狀態(tài)之外又又又把活承包給ZoneDelegate,而這次的承辦單位是ZoneDelegate.invokeTask
。
ZoneDelegate.invokeTask
相對比較簡單,我就不閹割它了。別看前面一堆判斷邏輯,都是虛張聲勢(也不全是,至少onInvokeTask這個勾子是此時被調(diào)用的)。ZoneDelegate.invokeTask
最重要的實(shí)際就是最后這句task.callback.apply(applyThis, applyArgs)
。這里的callback是setTimeout真實(shí)的回調(diào)函數(shù),從此出我們可以看出,這個回調(diào)函數(shù)確實(shí)是執(zhí)行在Zone的上下文中的。
代碼傳送門
你以為這就完了?
最后,這篇文章還差一個坑沒有填,那就是第三個要死記的_transitionTo
方法。之前只是說_transitionTo
可以改變Task的狀態(tài),那么一個Task到底有些狀態(tài)呢?都是什么時候改變的?下面這些是Task所有可能的狀態(tài),那我們對上面講的封裝邏輯只涉及到其中的幾個。
Task在剛剛初始化的時候是
notScheduled
的scheduleFn調(diào)度函數(shù)執(zhí)行之前,Task狀態(tài)會被改為
scheduling
scheduleFn調(diào)度函數(shù)執(zhí)行之后,Task狀態(tài)會被改為
scheduled
當(dāng)回調(diào)函數(shù)被置于調(diào)用棧中準(zhǔn)備執(zhí)行時,Task狀態(tài)會被改為
running
回調(diào)函數(shù)執(zhí)行完畢后,Task狀態(tài)會被改為
notScheduled
調(diào)用關(guān)系圖
最后,奉上我對源碼分析的調(diào)用關(guān)系圖

總結(jié)
分析zone.js源碼的過程是痛苦的,光從思維圖上就可以看出,zone.js的絕大多數(shù)邏輯都是圍繞Zone、ZoneDelegate、ZoneTask展開的。這兄弟三個之間相互引用、相互依賴,即使在我省略掉很多代碼之后還是存在很多錯綜復(fù)雜的調(diào)用關(guān)系。如果你是一個頸椎病患者,那么建議你可以深度體驗(yàn)一下,你的脖子大概率會問候一下zone.js的全體研發(fā)團(tuán)隊。
今天這篇文章其實(shí)只分析setTimeout的Patch邏輯,zone.js其實(shí)對很多其它API也都下手了。setTimeout只是一個宏任務(wù)的代表,后續(xù)希望可以再選一個微任務(wù)和事件繼續(xù)分析一下zone.js的打包流程。當(dāng)前,前提是if necessary
最近已經(jīng)梳理完NgZone的源碼邏輯,個人覺得可能會更貼近大家的實(shí)際開發(fā),分析過程也更有趣。喜歡的可以繼續(xù)蹲個后續(xù)~~~
OpenTiny 社區(qū)招募貢獻(xiàn)者啦
OpenTiny Vue 正在招募社區(qū)貢獻(xiàn)者,歡迎加入我們??
你可以通過以下方式參與貢獻(xiàn):
在 issue 列表中選擇自己喜歡的任務(wù)
閱讀貢獻(xiàn)者指南,開始參與貢獻(xiàn)
你可以根據(jù)自己的喜好認(rèn)領(lǐng)以下類型的任務(wù):
編寫單元測試
修復(fù)組件缺陷
為組件添加新特性
完善組件的文檔
如何貢獻(xiàn)單元測試:
在
packages/vue
目錄下搜索it.todo
關(guān)鍵字,找到待補(bǔ)充的單元測試按照以上指南編寫組件單元測試
執(zhí)行單個組件的單元測試:
pnpm test:unit3 button
如果你是一位經(jīng)驗(yàn)豐富的開發(fā)者,想接受一些有挑戰(zhàn)的任務(wù),可以考慮以下任務(wù):
? [Feature]: 希望提供 Skeleton 骨架屏組件
? [Feature]: 希望提供 Divider 分割線組件
? [Feature]: tree樹形控件能增加虛擬滾動功能
? [Feature]: 增加視頻播放組件
? [Feature]: 增加思維導(dǎo)圖組件
? [Feature]: 添加類似飛書的多維表格組件
? [Feature]: 添加到 unplugin-vue-components
? [Feature]: 兼容formily
參與 OpenTiny 開源社區(qū)貢獻(xiàn),你將收獲:
直接的價值:
通過參與一個實(shí)際的跨端、跨框架組件庫項目,學(xué)習(xí)最新的
Vite
+Vue3
+TypeScript
+Vitest
技術(shù)學(xué)習(xí)從 0 到 1 搭建一個自己的組件庫的整套流程和方法論,包括組件庫工程化、組件的設(shè)計和開發(fā)等
為自己的簡歷和職業(yè)生涯添彩,參與過優(yōu)秀的開源項目,這本身就是受面試官青睞的亮點(diǎn)
結(jié)識一群優(yōu)秀的、熱愛學(xué)習(xí)、熱愛開源的小伙伴,大家一起打造一個偉大的產(chǎn)品
長遠(yuǎn)的價值:
打造個人品牌,提升個人影響力
培養(yǎng)良好的編碼習(xí)慣
獲得華為云 OpenTiny 團(tuán)隊的榮譽(yù)和定制小禮物
受邀參加各類技術(shù)大會
成為 PMC 和 Committer 之后還能參與 OpenTiny 整個開源生態(tài)的決策和長遠(yuǎn)規(guī)劃,培養(yǎng)自己的管理和規(guī)劃能力
未來有更多機(jī)會和可能
關(guān)于 OpenTiny
OpenTiny 是一套企業(yè)級組件庫解決方案,適配 PC 端 / 移動端等多端,涵蓋 Vue2 / Vue3 / Angular 多技術(shù)棧,擁有主題配置系統(tǒng) / 中后臺模板 / CLI 命令行等效率提升工具,可幫助開發(fā)者高效開發(fā) Web 應(yīng)用。
核心亮點(diǎn):
跨端跨框架
:使用 Renderless 無渲染組件設(shè)計架構(gòu),實(shí)現(xiàn)了一套代碼同時支持 Vue2 / Vue3,PC / Mobile 端,并支持函數(shù)級別的邏輯定制和全模板替換,靈活性好、二次開發(fā)能力強(qiáng)。組件豐富
:PC 端有100+組件,移動端有30+組件,包含高頻組件 Table、Tree、Select 等,內(nèi)置虛擬滾動,保證大數(shù)據(jù)場景下的流暢體驗(yàn),除了業(yè)界常見組件之外,我們還提供了一些獨(dú)有的特色組件,如:Split 面板分割器、IpAddress IP地址輸入框、Calendar 日歷、Crop 圖片裁切等配置式組件
:組件支持模板式和配置式兩種使用方式,適合低代碼平臺,目前團(tuán)隊已經(jīng)將 OpenTiny 集成到內(nèi)部的低代碼平臺,針對低碼平臺做了大量優(yōu)化周邊生態(tài)齊全
:提供了基于 Angular + TypeScript 的 TinyNG 組件庫,提供包含 10+ 實(shí)用功能、20+ 典型頁面的 TinyPro 中后臺模板,提供覆蓋前端開發(fā)全流程的 TinyCLI 工程化工具,提供強(qiáng)大的在線主題配置平臺 TinyTheme
聯(lián)系我們:
官方公眾號:
OpenTiny
OpenTiny 官網(wǎng):https://opentiny.design/
OpenTiny 代碼倉庫:https://github.com/opentiny/
Vue 組件庫:https://github.com/opentiny/tiny-vue?(歡迎 Star)
Angluar組件庫:https://github.com/opentiny/ng?(歡迎 Star)
CLI工具:https://github.com/opentiny/tiny-cli?(歡迎 Star)
更多視頻內(nèi)容也可以關(guān)注OpenTiny社區(qū),B站/抖音/小紅書/視頻號。