zone.js由入門到放棄之一——通過一場游戲認識zone.js

之前有寫過一些介紹Angular中一些理念的文章,接下來我們來聊聊Angular中的一些依賴,比如zone.js。它是一個跨多個異步任務的執(zhí)行上下文,在攔截或追蹤異步任務方面有著特別強大的能力。來跟著嘯達同學的文章,一起了解一下吧~
前言
最近一段時間因為工作上的安排,需要研究Angular中的一些內部機制和模塊。Angular作為一個專門為大型前端項目而設計的優(yōu)秀框架,實際上有很多值得大家學習和借鑒的優(yōu)點的。之前了解到Angular的變更檢測跟Vue和React有本質的區(qū)別,而Angular的檢測體系是離不開zone.js的,所以本系列就針對zone.js進行一些分享,也希望能夠隨著個人對zone.js逐步學習制作一個由淺入深地學習指導,歡迎大家積極上車,一起學習、探討。

為什么要學習zone.js
這個系列的文章會包含大量的猜想、驗證、demo和源碼分析。在我自己學習的過程中,也出現(xiàn)過多次想要放棄,或者覺得差不多就行了的想法。所以如果想要堅持一件事,需要有一些明確的動機,畢竟動機不純,想裝純也難。那么就我個人而言,除了工作上的需要之外,我覺得有以下兩點驅動力:
動機一
17年的時候,有幸參加了一次大廠的面試,技面的時候我被問到Angular是如何處理變更檢測的。我對這塊的知識十分模糊,所以亂答了一氣。我把腦子里跟變更檢測相關的詞都掏了出來,結果越描越黑,最后不能自圓其說。面試官跟我說,希望我以后把這塊內容理理順。我答應了他,但沒想到竟是6年以后。
動機二
相信每個Angular的開發(fā)者都會見過類似下面這樣的報錯信息,甚至有些初學Angular的同事也因此覺得Angular的學習曲線比較陡峭、錯誤信息極不友好。其實,大家認為這些不友好的錯誤信息正是zone.js強大的地方。在不了解zone.js的前提下,這確實有點反人類。所以希望在學完這個系列后,不僅知道這樣的錯誤意味著什么,還能清楚這樣的問題是怎么產(chǎn)生的。
簡單認識一下
我不太會抽象概括,幸好Angular團隊對zone.js的定義只有一句,但是它簡單抽象到讓你看了和沒看都沒什么區(qū)別:
A Zone is an execution context that persists across async tasks. You can think of it as?thread-local storage?for JavaScript VMs.
到目前為止,我覺得大家也不必太在意這里描述了個啥,后面我會用其它的方式讓你漸漸了解它。這里姑且對幾個詞有點印象即可:
執(zhí)行上下文:execute context
保持:persist
異步任務:async tasks
有Java背景的可以回憶一下ThreadLocal的作用和用法;用過JS沙箱的也可以類比一下。都沒用過的也不礙事,不影響后面的閱讀。
PS:Angular團隊對zone.js還有個視頻介紹,建議大家可以等看完這篇文章后去了解一下。
ngZone和zone.js,傻傻分不清
最后在真正開始之前我要再補充一個知識點,有些人在學習zone.js的還見過ngZone這個東西,就認為這兩個是一個東西。這里做個簡單的聲明,Angular團隊基于zone.js構建了ngZone服務。NgZone定義Angular的執(zhí)行上下文,可以先簡單理解為是一個專門給Angular使用的定制化以后的zone.js。那么關于ngZone的知識,可以關注一下系列四和西系列五(如果有的話),在那里會有對ngZone和Angular具體的變更檢測方法做詳細介紹。
所以一句話概括兩者的關系,ngZone生于zone.js;長于Angular(生于斯,長于斯)。
從一個游戲開始了解zone.js
本文由真實事跡改編,如有雷同,絕非偶然
2022年底,本人工作的部門組織了一場switch對決賽。游戲有A、B兩支參賽隊伍,每支隊伍15人。要求每天兩隊之間進行3場對決,比賽一共會進行5一天,取得積分優(yōu)勢的團隊獲勝(輸?shù)哪顷犚堏A的隊伍吃飯)。主辦方提供了3款游戲:第一場回旋鏢;第二場馬8;第三場明星大亂斗。
如果只是關心哪里有這么好的部門的話,可以直接留言

既然游戲的種類和比賽順序是固定的,那么每天各隊派出的3位參賽選手的順序就很重要:比如可以讓熟悉某款游戲的選手去比對應的游戲;或者通過田忌賽馬的方式耗費對方的潛力。那么,我們今天的示例就從隊長選人開始:
第一版:無腦排兵被偷窺
這里我們有兩支參賽隊伍teamA、teamB;這里假設teamA只能順序排人,teamB只能倒序排人。同時還有一個裁判,負責收集teamA、teamB兩支隊每天的排序情況。下圖中代碼大概意思就是,隨著裁判一聲令下,teamA、teamB(代碼AB是順序執(zhí)行的,但是讀者在這里先別糾結)分別開始排名布陣,排好之后,裁判函數(shù)打印兩隊排序:
但是偏偏有年輕人不講武德,耗子尾汁,在兩隊排兵期間偷窺對方陣容:如上文中注釋代碼所示,teamB的隊長在teamA排陣過程中悄悄打印出teamA的陣型,導致teamB隊長可以針對性地進行兵力調整,已達到最好的效果。
這里真的想點名批評一下伍隊長,不講武德的人就是你
那么造成上述問題的原因主要是因為teamA和teamB對彼此都是可見的,即兩隊在排兵布陣的過程對對方是完全裸露的,導致讓對手有了可乘之機,所以這也是接下來要調整的重點。
第二版:小黑屋中探討軍機
為了不讓兩隊在排兵中知曉對方的陣容,需要將兩隊進行隔離。JS中進行數(shù)據(jù)隔離有很多辦法,從早期的閉包、后續(xù)有了通過模塊(文件)進行隔離,到現(xiàn)在在JS中也可以使用面向對象的編程思想。這里,我們就先通過模塊將兩隊隔離起來,文件結構如下:
teamA與tramB中代碼類似:
這一次,通過裁判程序將teamA和teamB導入,各隊排序過程相對獨立不受干擾。
第二版:容我想想
隔離的問題雖然解決了,這時teamB的隊長覺得每次排序都太倉促了,需要把人員排序的任務領回去跟團隊協(xié)商一下才行。這里我們使用異步任務來模擬各位隊長將任務帶回去排序的效果。
文件結構如下:
這一次,我們新增一個冥想程序:thinking.js,這里提供一個函數(shù),通過異步的setTimeout隨機等待03秒。兩位隊長針對每次比賽的出場人員順序進行認真地思考,這里使用延時03秒的thinking.js模塊模擬隊長做出決定的過程。
隊長代碼示例:
到這里,兩個隊長把各組的任務領回去了,可對戰(zhàn)室里還有個裁判呢。因為之前AB兩組瞬間就把陣容排好了,裁判馬上就能知道各隊的排序結果?,F(xiàn)在大家都回去各排各的了,把裁判一個人晾這了。而且,這個裁判不知道要等多久兩位隊長才能把人排完(每個隊長都需要0~3秒),所以裁判只能無奈地按照最長時間進行等待,即裁判要等待3秒再回來收集大家的結果:
第三版:zone.js版本
當你覺得所有人應該都滿足的時候,裁判站出來了說他不滿意。裁判不愿意一直在那里傻等結果,希望大家能在排序完畢后第一時間通知他,避免浪費時間。接下來就來看一下zone.js是如何解決這些問題的。
PS:這里大家先不要糾結API的用法和一些具體概念,后面的文章會一點一點給大家掃盲,先通過示例感受一下zone.js的功能。

示范前還是再澄清一下幾個比較重要的需求:
兩隊的排序數(shù)據(jù)需要隔離
兩隊排序時需要有思考時間(0~3s)
裁判要第一時間知道兩隊排序已結束,并公布結果
前文介紹中說過,zone.js的一個關鍵概念是執(zhí)行上下文,當時我們說可以把這個異步上下文類比成Java的LocalThread,即可以在單個線程內共享數(shù)據(jù)。那么在JS中,這個執(zhí)行上下文也是有類比的,可以把它想象成一個沙箱——一個JS的VM。在這個沙箱中,你可以把你的JS代碼放在沙箱中運行,同時沙箱也有一個上下文的概念,這是一段共享的內存空間,可以供運行在沙箱中的代碼所使用;同時沙箱和沙箱之間相互隔離、無法相互干擾。
Mark1:創(chuàng)建一個zone,zone.js通過fork方法可以創(chuàng)建一個zone,我們可以先理解它就是一個沙箱。
Mark2:zone.js中有一個靜態(tài)方法,可以獲取到zone,Zone.current
有了這兩個方法,就可以分別為teamA和teamB創(chuàng)建兩個zone。通過下面示例可見,代碼中分別創(chuàng)建兩個zone,他們分別持有teamA和teamB的對象,而teamA、B對象保存在zone的properties中。
上面代碼中,teamA和teamB不再被分別定義到兩個文件中了,為了驗證兩個隊伍的數(shù)據(jù)是否能夠被不同的zone隔離開,我們分別在zoneA和zoneB中執(zhí)行相同的代碼(打印properties中的組名)。為此,我們還需要了解一下zone.js提供的另外兩個API。
Mark3:zone.js提供一個run方法,可以在zone中執(zhí)行一段代碼
Mark4:zone.js提供一個get方法,可以獲取當前zone的properties屬性
可以看到,兩個zone中數(shù)據(jù)相互隔離,在run的作用域中,只能獲取到自己zone中的數(shù)據(jù)。
第一步改造
這里,我們首先做到了讓teamA和teamB的數(shù)據(jù)隔離。兩位隊長把每隊的人員信息都保存在各自的zone中,并在各自zone的上文中執(zhí)行排序任務。整個任務期間,兩個zone互不干涉。
但是上述代碼還有倆個問題:
裁判還是要等待3s后才能知道兩位隊長的人員排序
由于數(shù)據(jù)隔離,裁判也不知道兩位隊長的人員排序結果。裁判只能委托兩個隊長自己打印排序結果
有沒有辦法讓裁判自己能感知到兩位隊長何時排序結束,然后在兩位隊長排序完第一時間宣布排序結果呢?
其實如果大家仔細看下zone.js對fork方法的定義后就能知道,fork實際上只是創(chuàng)建出一個child zone。zone.js在初始化的時候回創(chuàng)建一個根zone,然后所有的通過fork后會在根zone下創(chuàng)建一個子zone。也就是說,zone是具有繼承關系的,官方習慣把這種關系叫做zone的可組合性。而每個子zone都保存了其父zone的對象;每個父zone也能監(jiān)聽到子zone的事件。
Mark5:可組合性:每個子zone都保存了其父zone的引用;每個父zone也能監(jiān)聽到子zone的事件。
每個子zone都保存了其父zone的引用這個好理解,那么每個父zone也能監(jiān)聽到子zone的事件怎么理解?其實這個就是zone.js最神奇的地方,zone.js在初始化的時候對很多API都做了“手腳”——Monkey Patch,將這些異步方法封裝成了zone.js中的異步任務。同時,由于在這些任務中定義很多勾子函數(shù),導致zone.js可以完全監(jiān)控這些異步任務的整個生命周期。
Mark6:追蹤異步任務
正是由于zone的這種特性,使得zone被經(jīng)常地用于異步任務的跟蹤和調試中。比如上文在動機2中展示的那個難以理解的錯誤堆棧,就是zone跟蹤異步異常的結果。
終極改造
最后這一版,我們給裁判也fork出一個zone,而teamA和tramB的zone都是fork自裁判zone的。這么處理后,裁判zone中就可以檢測到teamA和tramB中異步任務執(zhí)行的全部生命周期。其中,示例中只是用了zone.js眾多勾子中的一個——onHasTask
。這個函數(shù)會在執(zhí)行隊列中加入函數(shù)或沒有函數(shù)時被調用。
本例中,teamA執(zhí)行完畢后會把執(zhí)行結果更新到裁判zone中;teamB也做同樣的事。當兩隊都結束排序后,裁判zone通過配置的回調函數(shù)第一時間打印兩位隊長的排序結果。至此,該示例滿足了我們上述的所有需求。
源碼奉上:
總結
自此,通過一個小例子展示了一下zone.js的功能,同時根據(jù)例子淺述了一下zone.js的幾個特點。前文為了方便理解,一直把zone.js類比成LocalThread或者沙箱。其實,zone.js的能力遠不止這些類比的對象,它還被大量用在處理異步任務和異步錯誤的跟蹤中。至此,大家別忘了看下Angular團隊給出的zone.js的視頻介紹,可以更好地加深一下對本文的印象。
接下來,對于zone這個名字,個人感覺起的很到位(老外起名字總是很考究的)。zone被翻譯成地區(qū)、區(qū)域。就拿我們國家的區(qū)域劃分來說,國家、省、市、區(qū)、街道...每個同級的地區(qū)劃分都是相互隔離的,一級一級區(qū)域劃分又是可以嵌套的。不得不說,這種嵌套又隔離的特點在上面示例中展示的淋漓盡致。

這是本系列的第一篇文章,只是淺淺地入門了一下zone.js,后續(xù)會針對zone.js的API、源碼、以及如何跟Angular配合做進一步分析說明,感興趣的可以蹲個續(xù)~
關于 OpenTiny
OpenTiny 是一套企業(yè)級組件庫解決方案,適配 PC 端 / 移動端等多端,涵蓋 Vue2 / Vue3 / Angular 多技術棧,擁有主題配置系統(tǒng) / 中后臺模板 / CLI 命令行等效率提升工具,可幫助開發(fā)者高效開發(fā) Web 應用。
核心亮點:
跨端跨框架
:使用 Renderless 無渲染組件設計架構,實現(xiàn)了一套代碼同時支持 Vue2 / Vue3,PC / Mobile 端,并支持函數(shù)級別的邏輯定制和全模板替換,靈活性好、二次開發(fā)能力強。組件豐富
:PC 端有100+組件,移動端有30+組件,包含高頻組件 Table、Tree、Select 等,內置虛擬滾動,保證大數(shù)據(jù)場景下的流暢體驗,除了業(yè)界常見組件之外,我們還提供了一些獨有的特色組件,如:Split 面板分割器、IpAddress IP地址輸入框、Calendar 日歷、Crop 圖片裁切等配置式組件
:組件支持模板式和配置式兩種使用方式,適合低代碼平臺,目前團隊已經(jīng)將 OpenTiny 集成到內部的低代碼平臺,針對低碼平臺做了大量優(yōu)化周邊生態(tài)齊全
:提供了基于 Angular + TypeScript 的 TinyNG 組件庫,提供包含 10+ 實用功能、20+ 典型頁面的 TinyPro 中后臺模板,提供覆蓋前端開發(fā)全流程的 TinyCLI 工程化工具,提供強大的在線主題配置平臺 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)
更多視頻內容也可以關注OpenTiny社區(qū),B站/抖音/小紅書/視頻號。