概述
zone是異步任務(wù)中持續(xù)存在的執(zhí)行上下文
zone.js提供了一種機(jī)制來攔截異步任務(wù)以及追蹤異步任務(wù)
zone.js的代碼庫使用monkey patch的方式,在運(yùn)行時動態(tài)地給瀏覽器的異步api進(jìn)行一層包裝尸诽,并讓其在zone的上下文執(zhí)行夺克。通過指定攔截規(guī)則刻撒,能夠讓我們對異步操作的調(diào)用和調(diào)度進(jìn)行攔截,還可以在異步任務(wù)之前或之后添加代碼。一個系統(tǒng)中能夠存在多個zone實(shí)例,但是任意時刻只能有一個處于激活狀態(tài)尾组,通過Zone.current可以獲取當(dāng)前激活的zone實(shí)例。
zone.js所做的事情有如下幾點(diǎn):
1.攔截異步任務(wù)的調(diào)度
2.在異步操作中封裝回調(diào)函數(shù)示弓,以此來進(jìn)行錯誤處理和zone追蹤
3.提供一種方法添加數(shù)據(jù)到zones中去
4.提供最后一幀的錯誤處理的具體上下文
5.攔截阻塞的方法(alert/confirm/prompt/sync ajax)
一讳侨、封裝回調(diào)函數(shù)
zones需要在異步操作中持續(xù)存在,所以每次的異步任務(wù)建立時都需要捕獲當(dāng)前的zone并將其封裝到回調(diào)函數(shù)中奏属,在執(zhí)行異步任務(wù)時跨跨,將當(dāng)前的Zone.current恢復(fù)為之前捕獲的zone。所以如果一個異步操作鏈?zhǔn)且粋€執(zhí)行線程囱皿,那么Zone.current將充當(dāng)為線程的局部變量勇婴。
二、異步操作的調(diào)度
存在三種可以調(diào)度的異步任務(wù)
1.MicroTask:在當(dāng)前task結(jié)束之后和下一個task開始之前執(zhí)行的铆帽,不可取消咆耿,如Promise
,MutationObserver
爹橱、process.nextTick
2.MacroTask:一段時間后才執(zhí)行的task,可以取消,如setTimeout
, setInterval
, setImmediate
, I/O
, UI rendering
3.EventTask:監(jiān)聽未來的事件愧驱,可能執(zhí)行0次或多次,執(zhí)行時間是不確定的
zone.js對上述的api都進(jìn)行了monkey patch组砚,對這些api都進(jìn)行了重謝并替換了全局對象中的默認(rèn)方法
三吻商、可組合性
zones之間可以同過Zone.fork()組合在一起艾帐,一個子zone可以創(chuàng)建自己規(guī)則,可以:
1.將攔截委派給父zone,有選擇地在封裝回調(diào)之前或之后添加鉤子,
2.或者不用代理處理請求
組合性允許zones之間彼此互補(bǔ)干擾罐农,比如頂層的zone可以選擇捕獲錯誤条霜,子zone可以選擇追蹤用戶的行為
四、根zone
瀏覽器在開始運(yùn)行時會創(chuàng)建一個特殊的根zone涵亏,其余所有的zone都是根zone的子zone
五宰睡、分析
官方例子:profiling.html 計算異步任務(wù)的耗時
<!doctype html>
<html>
<head>
<meta charset="utf-8">
...
</head>
<body>
<h1>Profiling with Zones</h1>
<button id="b1">Start Profiling</button>
<script>
function sortAndPrintArray (unsortedArray) {
profilingZoneSpec.reset();
//執(zhí)行排序
asyncBogosort(unsortedArray, function (sortedArray) {
console.log(sortedArray);
//排序結(jié)束輸出耗時
console.log('sorting took ' + profilingZoneSpec.time() + ' of CPU time');
});
}
function asyncBogosort (arr, cb) {
//異步任務(wù)建立
setTimeout(function () {
if (isSorted(arr)) {
cb(arr);
} else {
var newArr = arr.slice(0);
newArr.sort(function () {
return Math.random() - 0.5;
});
asyncBogosort(newArr, cb);
}
}, 0);
}
function isSorted (things) {
for (var i = 1; i < things.length; i += 1) {
if (things[i] < things[i - 1]) {
return false;
}
}
return true;
}
//主函數(shù)
function main () {
var unsortedArray = [3,4,1,2,7];
//異步任務(wù)
b1.addEventListener('click', function () {
sortAndPrintArray(unsortedArray);
});
}
//返回zone的規(guī)則,并使用閉包維護(hù)了一個變量time來記錄時長
var profilingZoneSpec = (function () {
var time = 0,
//當(dāng)存在performance時使用performane的method溯乒,否則調(diào)用Date的method
//主要用于獲取當(dāng)前時間
timer = performance ?
performance.now.bind(performance) :
Date.now.bind(Date);
//返回zone的規(guī)則集
return {
//delegate類型是ZoneDelegate夹厌,每個zone都會有一個ZoneDelegate對象,主要
//為zone調(diào)用傳入的回調(diào)函數(shù)裆悄,建立矛纹、調(diào)用回調(diào)函數(shù)中的異步任務(wù),捕捉異步任
//務(wù)的錯誤光稼,這里傳入的delegate為父zone的代理對象或南。
//異步任務(wù)被調(diào)用前會執(zhí)行該函數(shù)
onInvokeTask: function (delegate, current, target, task, applyThis, applyArgs) {
this.start = timer(); //獲得開始時間
//可以讓父代理執(zhí)行異步回調(diào)也可自己執(zhí)行不使用代理
delegate.invokeTask(target, task, applyThis, applyArgs);
//異步回調(diào)執(zhí)行完畢計算耗時
time += timer() - this.start;
},
//獲取當(dāng)前耗時
time: function () {
return Math.floor(time*100) / 100 + 'ms';
},
//將當(dāng)前耗時置為零
reset: function () {
time = 0;
}
};
}());
//根zone創(chuàng)建了一個子zone,子zone執(zhí)行函數(shù)main
Zone.current.fork(profilingZoneSpec).run(main);
</script>
</body>
</html>
Zone.current.fork(profilingZoneSpec).run(main)
是這份代碼最重要的一句艾君,為了弄懂這句話究竟做了什么采够,先來看下zone.js的源碼實(shí)現(xiàn)
Class Zone:
...
private _zoneDelegate: ZoneDelegate;
static get current(): AmbientZone {
return _currentZoneFrame.zone;
}
public fork(zoneSpec: ZoneSpec): AmbientZone {
if (!zoneSpec) throw new Error('ZoneSpec required!');
return this._zoneDelegate.fork(this, zoneSpec);
}
public run<T>(callback: (...args: any[]) => T, applyThis: any = undefined, applyArgs: any[] = null
,source: string = null): T {
_currentZoneFrame = {parent: _currentZoneFrame, zone: this};
try {
return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);
} finally {
_currentZoneFrame = _currentZoneFrame.parent;
}
}
...
Class ZoneDelegate:
...
fork(targetZone: Zone, zoneSpec: ZoneSpec): AmbientZone {
return this._forkZS ? this._forkZS.onFork(this._forkDlgt, this.zone, targetZone, zoneSpec) :
new Zone(targetZone, zoneSpec);
}
invoke(targetZone: Zone, callback: Function, applyThis: any, applyArgs: any[], source: string):
any {
return this._invokeZS ?
this._invokeZS.onInvoke(
this._invokeDlgt, this._invokeCurrZone, targetZone, callback, applyThis, applyArgs,
source) :
callback.apply(applyThis, applyArgs);
}
...
如代碼所示,current
方法是Zone類的一個靜態(tài)方法冰垄,返回_currentZoneFrame.zone
蹬癌,_currentZoneFrame
是一個全局對象,保存了當(dāng)前系統(tǒng)中的zone幀鏈,它有兩個屬性逝薪,parent
指向了父zoneFrame隅要,zone
指向了當(dāng)前激活的zone對象。所以_currentZoneFrame
并不是固定不變的董济。
let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null, null)};
系統(tǒng)初始化時步清,實(shí)例化zone時,需往構(gòu)造函數(shù)傳入一個父zone對象和一個zone規(guī)則對象虏肾,當(dāng)zone規(guī)則對象為null時廓啊,構(gòu)造函數(shù)將認(rèn)為該zone是根zone。這也說明了為什么瀏覽器在開始運(yùn)行時會創(chuàng)建一個特殊的根zone封豪,因?yàn)樵诼暶?code>_currentZoneFrame時就創(chuàng)建了根zone谴轮。
所以 Zone.current.fork(profilingZoneSpec).run(main)
的意思就是,使用根zone創(chuàng)建新的未命名的子zone撑毛,然后讓子zone去運(yùn)行main()
public fork(zoneSpec: ZoneSpec): AmbientZone {
if (!zoneSpec) throw new Error('ZoneSpec required!');
return this._zoneDelegate.fork(this, zoneSpec); //體交由zone的代理對象來實(shí)現(xiàn)
}
從上面代碼中可以知道书聚,zone實(shí)例的fork方法中會交代給代理去創(chuàng)建新的子zone。因?yàn)閦one.js允許我們在新建子zone前添加hook藻雌,代理對象的fork方法會判斷是否有onFork的hook雌续,若有則先執(zhí)行onFork,如下所示
fork(targetZone: Zone, zoneSpec: ZoneSpec): AmbientZone {
return this._forkZS ? //fork規(guī)則是否存在
this._forkZS.onFork(this._forkDlgt, this.zone, targetZone, zoneSpec) :
new Zone(targetZone, zoneSpec);
}
當(dāng)發(fā)現(xiàn)規(guī)則中有onFork的要求時胯杭,則先執(zhí)行該hook驯杜。除此onFork之外,在攔截規(guī)則中我們同樣可以設(shè)置onInvoke做个、onHandleError鸽心、onInvokeTask、onCancelTask等hook居暖,原理同上顽频,zone都會將其交由代理來處理。例如onInvoke太闺,拿Zone.current.fork(profilingZoneSpec).run(main)
舉例糯景,run會調(diào)用Invoke方法,下面是run方法的具體實(shí)現(xiàn)
public run<T>(callback: (...args: any[]) => T, applyThis: any = undefined, applyArgs: any[] = null
,source: string = null): T {
_currentZoneFrame = {parent: _currentZoneFrame, zone: this};
try {
return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);
} finally {
_currentZoneFrame = _currentZoneFrame.parent;
}
}
可以看到省骂,執(zhí)行run(main)
之后蟀淮,zone將main的調(diào)用交給了代理對象,代理對象的invoke方法實(shí)現(xiàn)如下
invoke(targetZone: Zone, callback: Function, applyThis: any, applyArgs: any[], source: string):
any {
return this._invokeZS ?
this._invokeZS.onInvoke(
this._invokeDlgt, this._invokeCurrZone, targetZone, callback, applyThis, applyArgs,
source) :
callback.apply(applyThis, applyArgs); //直接執(zhí)行傳入的回調(diào)
}
當(dāng)代理對象發(fā)現(xiàn)規(guī)則中有onInvoke的hook時怠惶,則先執(zhí)行該hook。但是在該樣例中并沒有設(shè)置onInvoke轧粟,所以代理對象直接執(zhí)行了main策治。
到這里或許會很疑惑脓魏,zone執(zhí)行了main就結(jié)束了嗎?它是如何追蹤異步任務(wù)的览妖,答案是monkey patch轧拄,通過對異步任務(wù)的patch揽祥,在任務(wù)創(chuàng)建前和執(zhí)行前都進(jìn)行了一層封裝讽膏,下面來看zone.js是如何patch setTimeout的
//browser.ts
Zone.__load_patch('timers', (global: any) => {
const set = 'set';
const clear = 'clear';
patchTimer(global, set, clear, 'Timeout');
patchTimer(global, set, clear, 'Interval');
patchTimer(global, set, clear, 'Immediate');
});
patchTimer是實(shí)現(xiàn)patch的主要方法,參數(shù)global是window對象拄丰,set府树、clear是異步任務(wù)的前綴,最后一個參數(shù)是異步任務(wù)的后綴料按。接著看下patchTimer的部分實(shí)現(xiàn)
patchTimer(window: any, setName: string, cancelName: string, nameSuffix: string) {
let setNative: Function = null; //原生的setTimeout
let clearNative: Function = null; //原生的cleanTimeout
setName += nameSuffix; //獲得'setTimeout'
cancelName += nameSuffix; //獲得'cleanTimeout'
//該方法會在建立異步任務(wù)時被調(diào)用奄侠,具體可看官方源碼
function scheduleTask(task: Task) {
const data = <TimerOptions>task.data;
//要執(zhí)行的異步任務(wù)
function timer() {
try {
task.invoke.apply(this, arguments);
} finally {...}
}
}
}
data.args[0] = timer;
//調(diào)用原生setTimeout,將data.args[0] 即callback和data.args[1]即delay作為參數(shù)
data.handleId = setNative.apply(window, data.args);
return task;
}
setNative =
patchMethod(window, setName, (delegate: Function) => function(self: any, args: any[]) {}
clearNative =
patchMethod(window, cancelName, (delegate: Function) => function(self: any, args: any[]) {}
}
patchMethod
返回的是原生的setTimeout
载矿,同時patchMethod
會將window.setTimeout
進(jìn)行patch垄潮,下面是patch后的window.setTimeout
的部分代碼
window.setTimeout = function() {
return patchDelegate(this, arguments as any);
}
patchDelegate(self: any, args: any[]) {
if (typeof args[0] === 'function') {
const options: TimerOptions = {
handleId: null,
isPeriodic: nameSuffix === 'Interval',
delay: (nameSuffix === 'Timeout' || nameSuffix === 'Interval') ? args[1] || 0 : null,
args: args
};
//新建異步任務(wù)對象,同時在返回task前闷盔,會調(diào)用傳入的scheduleTask方法弯洗,
//scheduleTask方法會調(diào)用原生的setTimeout對象
const task =
scheduleMacroTaskWithCurrentZone(setName, args[0], options, scheduleTask, clearTask);
...
}
所以,當(dāng)我們調(diào)用全局的setTimeout時逢勾,就會將傳入的回調(diào)函數(shù)和延遲時間包裝為一個Task對象牡整,然后zone代理對象執(zhí)行Task的scheduleTask方法,scheduleTask方法又調(diào)用了原生setTimeout方法溺拱,然后setTimeout在一段時間后執(zhí)行Task的invoke方法逃贝,invoke方法里包裝了真正的回調(diào)函數(shù)。
最后說說官方例子中onInvokeTask
是什么時候執(zhí)行的
//代理對象的invokeTask方法
invokeTask(targetZone: Zone, task: Task, applyThis: any, applyArgs: any): any {
return this._invokeTaskZS ?
this._invokeTaskZS.onInvokeTask(
this._invokeTaskDlgt, this._invokeTaskCurrZone, targetZone, task, applyThis,
applyArgs) :
task.callback.apply(applyThis, applyArgs);
}
異步任務(wù)執(zhí)行時最后會經(jīng)過層層傳遞迫摔,最后交由代理對象來執(zhí)行沐扳,代理對象會先判斷是否有設(shè)置onInvokeTask
的hook,有則執(zhí)行onInvokeTask
句占,不執(zhí)行異步任務(wù)的回調(diào)函數(shù)沪摄。
onInvokeTask: function (parentdelegate, current, target, task, applyThis, applyArgs) {
this.start = timer();
//交由父代理來執(zhí)行異步任務(wù)
parentdelegate.invokeTask(target, task, applyThis, applyArgs);
time += timer() - this.start;
},
執(zhí)行onInvokeTask
時,會交由父代理來執(zhí)行異步任務(wù)辖众,因?yàn)榭赡艽嬖诟竮one中也設(shè)置了onInvokeTask
的情況卓起,直到有某個父zone沒有設(shè)置onInvokeTask
時,才真正執(zhí)行異步任務(wù)的回調(diào)函數(shù)凹炸。
參考:
Brian Ford Zone
zone.js
How the hell does zone.js really work?