zone.js 源碼初探

概述

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í)行的铆帽,不可取消咆耿,如PromiseMutationObserver爹橱、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?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末戏阅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子啤它,更是在濱河造成了極大的恐慌奕筐,老刑警劉巖舱痘,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異离赫,居然都是意外死亡芭逝,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進(jìn)店門渊胸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旬盯,“玉大人,你說我怎么就攤上這事翎猛∨趾玻” “怎么了?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵切厘,是天一觀的道長萨咳。 經(jīng)常有香客問我,道長疫稿,這世上最難降的妖魔是什么培他? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮遗座,結(jié)果婚禮上舀凛,老公的妹妹穿的比我還像新娘。我一直安慰自己员萍,他們只是感情好腾降,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著碎绎,像睡著了一般螃壤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上筋帖,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天奸晴,我揣著相機(jī)與錄音,去河邊找鬼日麸。 笑死寄啼,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的代箭。 我是一名探鬼主播墩划,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼嗡综!你這毒婦竟也來了乙帮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤极景,失蹤者是張志新(化名)和其女友劉穎察净,沒想到半個月后驾茴,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡氢卡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年锈至,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片译秦。...
    茶點(diǎn)故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡峡捡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诀浪,到底是詐尸還是另有隱情棋返,我是刑警寧澤,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布雷猪,位于F島的核電站,受9級特大地震影響晰房,放射性物質(zhì)發(fā)生泄漏求摇。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一殊者、第九天 我趴在偏房一處隱蔽的房頂上張望与境。 院中可真熱鬧,春花似錦猖吴、人聲如沸摔刁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽共屈。三九已至,卻和暖如春党窜,著一層夾襖步出監(jiān)牢的瞬間拗引,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工幌衣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留矾削,地道東北人。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓豁护,卻偏偏與公主長得像哼凯,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子楚里,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 1.ios高性能編程 (1).內(nèi)層 最小的內(nèi)層平均值和峰值(2).耗電量 高效的算法和數(shù)據(jù)結(jié)構(gòu)(3).初始化時...
    歐辰_OSR閱讀 29,386評論 8 265
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理断部,服務(wù)發(fā)現(xiàn),斷路器腻豌,智...
    卡卡羅2017閱讀 134,657評論 18 139
  • 原文地址:http://blog.thoughtram.io/angular/2016/01/22/underst...
    4ea0af17fd67閱讀 1,819評論 0 2
  • 如果周圍的人毫無理性地向你發(fā)難家坎,你仍能鎮(zhèn)定自若保持冷靜嘱能; 如果眾人對你心存猜忌,你仍能自信如常并認(rèn)為他們的猜忌情有...
    971fe2272a15閱讀 148評論 0 0
  • 我的國慶怎樣規(guī)劃? 華山歸來后讀書做瞪《苑啵可幾天過去了,書還沒有 讀呢装蓬。 那么著拭,明天就拿起來看吧。 這幾天都做啥了牍帚? 華...
    者行孫閱讀 318評論 0 1