这是来自孙啸达 同学的zone.js系列文章第二篇,这篇文章次要为咱们介绍了Zone和ZoneTask

zone.js系列往期文章

  • zone.js由入门到放弃之一——通过一场游戏意识zone.js

zone.js中最重要的三个定义为:Zone,ZoneDelegate,ZoneTask。搞清楚了这三个类的API及它们之间关系,基本上对zone.js就通了。而Zone,ZoneDelegate,ZoneTask三者中,Zone,ZoneDelegate其实半差不差的能够先当成一个货色。所以文中,咱们集中火力主攻Zone和ZoneTask。

Zone

传送门

interface Zone {  // 通用API  name: string;  get(key: string): any;  getZoneWith(key: string): Zone|null;  fork(zoneSpec: ZoneSpec): Zone;  run<T>(callback: Function, applyThis?: any, applyArgs?: any[], source?: string): T;  runGuarded<T>(callback: Function, applyThis?: any, applyArgs?: any[], source?: string): T;  runTask(task: Task, applyThis?: any, applyArgs?: any): any;  cancelTask(task: Task): any;  // Wrap类包装API  wrap<F extends Function>(callback: F, source: string): F;  // Task类包装API  scheduleMicroTask(      source: string, callback: Function, data?: TaskData,      customSchedule?: (task: Task) => void): MicroTask;  scheduleMacroTask(      source: string, callback: Function, data?: TaskData, customSchedule?: (task: Task) => void,      customCancel?: (task: Task) => void): MacroTask;  scheduleEventTask(      source: string, callback: Function, data?: TaskData, customSchedule?: (task: Task) => void,      customCancel?: (task: Task) => void): EventTask;  scheduleTask<T extends Task>(task: T): T;}

Zone中的API大抵分了三类:通用API、Wrap类和Task类。Wrap和Task类别离对应zone.js对异步办法的两种打包形式(Patch),不同的打包形式对异步回调提供了不同粒度的"监听"形式,即不同的打包形式会暴露出不同的拦挡勾子。你能够依据本身对异步的控制精度抉择不同的打包形式。

Wrap形式:

  • onInvoke
  • onIntercept

Task形式

  • onScheduleTask
  • onInvokeTask
  • onCancelTask
  • onHasTask

上文说到了,zone.js在初始化的时候曾经把大多数常见的异步API都打包过了(就是用的下面这些API打包的),除了这些默认被打包的API以外,zone.js也反对用户对一些自研的API或是一些依赖中API自行打包。下图展现了一些曾经被zone.js默认打包的API,感兴趣的能够理解一下:

通用API

zone.js的current和get在上一篇文章中曾经介绍过了,因为自身也不太难,这里就不专门举例了。

  • [ ]  current:获取以后的zone上下文
  • [ ]  get:从properties中获取以后zone中的属性。properties属性其实是immutable的,上一篇文章中间接对properties进行批改其实是不举荐的。同时,因为zone之间是能够通过fork嵌套的,所以子zone能够继承父zone的properties。
  • [ ]  fork(zoneSpec: ZoneSpec):fork办法能够给以后Zone创立一个子Zone,函数承受一个ZoneSpec的参数,参数规定了以后Zone的一些根本信息以及须要注入的勾子。上面展现了ZoneSpec的所有属性:

传送门

interface ZoneSpec {  name: string;  properties?: {[key: string]: any};  onFork?:      (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,       zoneSpec: ZoneSpec) => Zone;  onIntercept?:      (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,       source: string) => Function;  onInvoke?:      (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function,       applyThis: any, applyArgs?: any[], source?: string) => any;  onHandleError?:      (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,       error: any) => boolean;  onScheduleTask?:      (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) => Task;  onInvokeTask?:      (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task,       applyThis: any, applyArgs?: any[]) => any;  onCancelTask?:      (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) => any;  onHasTask?:      (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone,       hasTaskState: HasTaskState) => void;}
  •  run(callback: Function, applyThis?: any, applyArgs?: any[], source?: string): T;
  •  runGuarded(callback: Function, applyThis?: any, applyArgs?: any[], source?: string): T;
  •  runTask(task: Task, applyThis?: any, applyArgs?: any;

runXXX办法能够指定函数运行在特定的zone上,这里能够把该办法类比成JS中的call或者apply,它能够指定函数所运行的上下文环境;而zone在这里能够类比成非凡的this,只不过zone上下文能够跨执行栈保留,而this不行。与此同时,runXXX在回调执行完结后,会主动地复原zone的执行环境。

Demo1:zone的一些基操

看过一篇的对这个例子应该不生疏了,这个例子次要演示了如何通过zone.js的通用API创立zone,并在特定的zone上下文中执行函数。

// 创立子zoneconst apiZone = Zone.current.fork({  name: 'api',  // 通过ZoneSpec设置属性  properties: {    section: 'section1',  },});apiZone.run(() => {  const currentZone = Zone.current;  assert.equal(currentZone.name, 'api');  assert.equal(currentZone.get('section'), 'section1');});
Demo2:runXXX
  •  wrap(callback: F, source: string): F;

前文说了runXXX办法相似于call和apply的作用,那么wrap办法相似于JS中的bind办法。wrap能够将执行函数绑定到以后的zone中,使得函数也能执行在特定的zone中。上面是我简化当前的wrap源码:

public wrap<T extends Function>(callback: T, source: string): T {  // 省略若干无关紧要的代码  const zone: Zone = Zone.current;  return function() {    return zone.runGuarded(callback, (this as any), <any>arguments, source);  } as any as T;}

wrap自身却是什么也没做,只是保护了对runGuarded办法的调用。runGuarded办法其实也没有什么神奇之处,它外部就是对run办法的一个调用,只不过runGuarded办法会尝试捕捉一下run办法执行过程中抛出的异样。上面是run和runGuarded的源码比拟,看下runGuarded比照run是不是就多了一个catch?

传送门

public run<T>(callback: (...args: any[]) => T, applyThis?: any, applyArgs?: any[], source?: string): T {  _currentZoneFrame = {parent: _currentZoneFrame, zone: this};  try {    return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);  } finally {    _currentZoneFrame = _currentZoneFrame.parent!;  }}public runGuarded<T>(    callback: (...args: any[]) => T, applyThis: any = null, applyArgs?: any[],    source?: string) {  _currentZoneFrame = {parent: _currentZoneFrame, zone: this};  try {    try {      return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source);    } catch (error) {      if (this._zoneDelegate.handleError(this, error)) {        throw error;      }    }  } finally {    _currentZoneFrame = _currentZoneFrame.parent!;  }}
Demo3:onHandleError

下面介绍,run和runGuarded就只差一个catch,那么这个catch中调用的handleError办法又是做什么的?其实handleError理论触发的是zone中的一个钩子函数onHandleError。咱们能够在定义一个zone的时候将其定义在zoneSpec中,此时,当函数运行过程中呈现了未捕捉异样的时候,该钩子函数会被触发。留神,这里是未捕捉的异样,如果异样曾经被捕捉,则该钩子不会触发。感兴趣的能够在reject前面间接catch异样,看下此时onHandleError还会不会执行。

// 创立子zoneconst apiZone = Zone.current.fork({  name: 'api',  // 通过ZoneSpec设置属性  properties: {    section: 'section1',  },  onHandleError: function (parentZoneDelegate, currentZone, targetZone, error) {    console.log(`onHandleError catch: ${error}`);    return parentZoneDelegate.handleError(targetZone, error);  }});apiZone.run(() => {  Promise.reject('promise error');});// onHandleError catch: Error: Uncaught (in promise): promise error// Unhandled Promise rejection: promise error ; Zone: api ; Task: null ; Value: promise error undefine
Demo4: onIntercept & onInvoke
  •  onIntercept:当在注册回调函数时被触发,简略点了解在调用wrap的时候,该勾子被调用
  •  onInvoke: 当通过wrap包装的函数调用时被触发

onIntercept个别用的很少,我也没有想到特地好的应用场景。上面这个例子通过onIntercept勾子“重定义”了回调函数,在回调函数之前又加了一段打印。所以集体认为,onIntercept能够用来对包装函数做一些通用的AOP加强。

onInvoke会在下一篇源码剖析中大量呈现,每当包装函数要执行时就会触发Zone(理论是ZoneDelegate)的invoke办法时,介时onInvoke勾子办法就会被调用。

上面的例子中,先通过wrap函数将setTimeout的回调包装,并将回调的执行绑定到apiZone上。当回调函数执行时,onInvoke被调用。这里通过onInvoke勾子打印了一下回调执行工夫,从而侧面阐明了onInvoke的执行机会。

const apiZone = Zone.current.fork({  name: 'api',  onIntercept: function (_parentZoneDelegate, currentZone, targetZone, delegate, source) {    console.log('Enter onIntercept', currentZone.name, Date.now() - start);    // 批改原回调实现    function newCb() {      console.log('hacking something in main');      delegate.call(this);    }    return newCb;  },  onInvoke: function (parentZoneDelegate, currentZone, targetZone, delegate, applyThis, applyArgs, source) {    console.log('Enter onInvoke', currentZone.name, Date.now() - start);    parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source);  },});const cb = function() {  console.log('cb called', Zone.current.name, Date.now() - start);};function main() {  setTimeout(apiZone.wrap(cb), 1000);}const start = Date.now();main();// Enter onIntercept api 0// Enter onInvoke api 1010// hacking something in main// cb called api 1010

讲到这里Zone的通用API和Wrap打包形式就讲完了,置信大家都有点累,劳动一下吧

ZoneTask

zone.js打包了大多数你见到过的异步办法,其中有很大一部分被打包成Task的模式。Task模式比Wrap模式有更丰盛的生命周期勾子,使得你能够更精细化地管制每个异步工作。好比Angular,它能够通过这些勾子决定在何时进行脏值检测,何时渲染UI界面。

zone.js工作分成MacroTask、MicroTask和EventTask三种:

  • MicroTask:在以后task完结之后和下一个task开始之前执行的,不可勾销,如Promise,MutationObserver、process.nextTick
  • MacroTask:一段时间后才执行的task,能够勾销,如setTimeout, setInterval, setImmediate, I/O, UI rendering
  • EventTask:监听将来的事件,可能执行0次或屡次,执行工夫是不确定的
Demo5:SetTimeout Task

zone.js对Task的生命周期勾子:

  •  onScheduleTask:当一步操作被探测出的时候调用
  •  onInvokeTask:当回调执行时被调用
  •  onHasTask:当队列状态产生扭转时被调用

单看对着三个勾子函数的介绍,很难分明地意识到他们的意思和触发机会。我以setTimeout为例,介绍一下我对这几个勾子的了解,这里会波及到一些源码逻辑,这些会在第三篇文章中具体阐明,这里理解个大略即可。

首先,zone初始化的时候会monkey patch原生的setTimeout办法。之后,每当setTimeout被调用时,patch后的办法都会把以后的异步操作打包成Task,在调用真正的setTimeout之前会触发onScheduleTask。

将setTimeout打包成Task后,这个异步工作就会进入到zone的管控之中。接下来,Task会将setTimeout回调通过wrap打包,所以当回调执行时,zone也是能够感知的。当回调被执行之前,onInvokeTask勾子会被触发。onInvokeTask执行完结后,才会执行真正的setTimeout回调。

onHasTask这个勾子比拟有意思,它记录了工作队列的状态。当工作队列中有MacroTask、MicroTask或EventTask进队或出队时都会触发该勾子函数。

下图是一个onHasTask中保护队列状态的示例,该状态表明了有一个MacroTask工作进入了队列。

{  microTask: false,   macroTask: true, // macroTask进入队列  eventTask: false,  change: 'macroTask' // 本次事件由哪种工作触发}

这是一个MacroTask出队的示例:

{  microTask: false,   macroTask: false, // macroTask 出队列  eventTask: false,  change: 'macroTask' // 本次事件由哪种工作触发}

上面这个示例onHasTask被调用两次,第一次是setTimeout工夫进入工作队列;第二次是setTimeout执行结束,移出工作队列。同时在onScheduleTask和onInvokeTask中,也能够通过task.type获取到以后的工作类型。

const apiZone = Zone.current.fork({  name: 'apiZone',  onScheduleTask(delegate, current, target, task) {    console.log('onScheduleTask: ', task.type, task.source, Date.now() - start);    return delegate.scheduleTask(target, task);  },  onInvokeTask(delegate, current, target, task, applyThis, applyArgs) {    console.log('onInvokeTask: ', task.type, task.source, Date.now() - start);    return delegate.invokeTask(target, task, applyThis, applyArgs);  },  onHasTask(delegate, current, target, hasTaskState) {    console.log('onHasTask: ', hasTaskState, Date.now() - start);    return delegate.hasTask(target, hasTaskState);  }});const start = Date.now();apiZone.run(() => {  setTimeout(function() {    console.log('setTimeout called');  }, 1000);});// onScheduleTask:  macroTask setTimeout 0// onHasTask:  {//   microTask: false,//   macroTask: true,//   eventTask: false,//   change: 'macroTask'// } 4// onInvokeTask:  macroTask setTimeout 1018// setTimeout called// onHasTask:  {//   microTask: false,//   macroTask: false,//   eventTask: false,//   change: 'macroTask'// } 1018
Demo6:多任务跟踪

为了能认清zone.js对跟异步工作的跟踪能力,咱们模仿多个、多种异步工作,测试一下zone.js对这些工作的跟踪能力。上面例子,zone.js别离监控了5个setTimeout工作和5个Promise工作。从后果上看,zone外部能够分明地晓得各种类型的工作什么时候创立、什么时候执行、什么时候销毁。Angular正是基于这一点进行变更检测的,ngZone中的stable状态也是由此产生的,这个咱们会在系列的第四篇中介绍。

// 宏工作计数let macroTaskCount = 0;// 微工作计数let microTaskCount = 0;const apiZone = Zone.current.fork({  name: 'apiZone',  onScheduleTask: (delegate, currZone, target, task) => {    if (task.type === 'macroTask') {      macroTaskCount ++;      console.log('A new macroTask is scheduled: ' + macroTaskCount);    } else if (task.type === 'microTask') {      microTaskCount ++;      console.log('A new microTask is scheduled: ' + microTaskCount);    }    return delegate.scheduleTask(target, task);  },  onInvokeTask: (delegate, currZone, target, task, applyThis, applyArgs) => {    const result = delegate.invokeTask(target, task, applyThis, applyArgs);     if (task.type === 'macroTask') {      macroTaskCount --;      console.log('A macroTask is invoked: ' + macroTaskCount);    } else if (task.type === 'microTask') {      microTaskCount --;      console.log('A microTask is invoked: ' + microTaskCount);    }    return result;  },});apiZone.run(() => {  for (let i = 0; i < 5; i ++) {    setTimeout(() => {    });  }  for (let i = 0; i < 5; i ++) {    Promise.resolve().then(() => {    });  }});// A new macroTask is scheduled: 1// A new macroTask is scheduled: 2// A new macroTask is scheduled: 3// A new macroTask is scheduled: 4// A new macroTask is scheduled: 5// A new microTask is scheduled: 1// A new microTask is scheduled: 2// A new microTask is scheduled: 3// A new microTask is scheduled: 4// A new microTask is scheduled: 5// A microTask is invoked: 4// A microTask is invoked: 3// A microTask is invoked: 2// A microTask is invoked: 1// A microTask is invoked: 0// A macroTask is invoked: 4// A macroTask is invoked: 3// A macroTask is invoked: 2// A macroTask is invoked: 1// A macroTask is invoked: 0
Demo7:手动打包setTimeout

咱们最初还有3个API没有讲:scheduleMacroTask、scheduleMicroTask、scheduleEventTask。zone.js通过这三个办法将一般的异步办法打包成异步工作。这三个办法属于比拟底层的API,个别很少会用,因为大部分API的打包zone曾经帮咱们实现了。为了介绍一下这个API的应用,明天就头铁一次,应用scheduleMacroTask打包一个咱们本人的setTimeout。

筹备

咱们晓得,zone.js默认会打包setTimeout的,打包后的setTimeout变成Task被管控起来。所以,咱们能够通过Task的勾子有没有触发判断setTimeout有没有被打包。上面代码为例,当onHasTask事件触发,咱们能力判定setTimeout曾经被打包成Task。

const apiZone = Zone.current.fork({  name: 'api',  onHasTask: function (parentZoneDelegate, currentZone, targetZone, hasTaskState) {    console.log(hasTaskState);    parentZoneDelegate.onHasTask(parentZoneDelegate, currentZone, targetZone, hasTaskState);  }});apiZone.run(() => {  setTimeout(() => {    console.log(Zone.current.name);  });});// {//   microTask: false,//   macroTask: true,//   eventTask: false,//   change: 'macroTask'// }

Step1:勾销zone.js原生打包

zone.js初始化时会主动打包setTimeout函数,所以咱们第一步要做的就是禁止zone.js主动打包setTimeout。自zone.js v0.8.9当前,zone.js反对用户通过配置自主抉择须要打包的函数。比方本例中,只须要对__Zone_disable_timers进行设置就能够敞开zone.js对setTimeout的打包。

global.__Zone_disable_timers = true;

Step2:移花接木

革新setTimeout的第一步就是要保留原始的setTimeout:

const originalSetTineout = global.setTimeout;

Step3:scheduleMacroTask

scheduleMacroTask用来将异步办法打包成Task。值得注意的是scheduleMacroTask的最初一个参数,要求传入一个Task的调度办法。这个办法返回了原生的setTimeout办法,只是把回调函数换成了task.invoke。看到这,你对zone.js的意识应该越来越清晰了,task.invoke中就是zone对回调函数的打包。打包的后果就是让回调能够在正确地zone上下文中被执行。

Zone.current.scheduleMacroTask('setTimeout', cb, taskOptions, function(task) {    return originalSetTineout(task.invoke, delay);});

最初,残缺代码奉上:

// 禁止zone.js的默认打包行为global.__Zone_disable_timers = true;require('zone.js');function myPatchTimer() {  // 保留原有setTimeout函数  const originalSetTineout = global.setTimeout;  global.setTimeout = function(cb, delay) {    const taskOptions = {      isPeriodic: false, // 是否是间歇性的,相似setInterval    };    // 将异步函数打包成Task    Zone.current.scheduleMacroTask('setTimeout', cb, taskOptions, function(task) {      // task.invoke能够跨调用栈保留zone上下文      return originalSetTineout(task.invoke, delay);    });  };}myPatchTimer();const apiZone = Zone.current.fork({  name: 'api',  onHasTask: function (parentZoneDelegate, currentZone, targetZone, hasTaskState) {    console.log(hasTaskState);    parentZoneDelegate.onHasTask(parentZoneDelegate, currentZone, targetZone, hasTaskState);  }});apiZone.run(() => {  setTimeout(() => {    console.log(Zone.current.name);  }, 1000);});

总结

本文重点介绍了zone.js中各个API的应用形式及相互间关系,通过大量的试验demo简略演示了一下这些API的用法。最初,还通过一个较底层的API打包了本人的Task。当然,本文最初这个对setTimeout的打包还是太过毛糙,原生的打包要比这个简单的多。即便如此,我置信看到这里的童鞋们应该曾经对zone.js背地的逻辑有了肯定的意识了。

下一篇文章,我筹备对zone.js的打包原理做更深的剖析。大家能够跟我一起深刻到源码看看zone.js在打包setTimeout时做了哪些工作。

对于 OpenTiny

OpenTiny 是一套企业级组件库解决方案,适配 PC 端 / 挪动端等多端,涵盖 Vue2 / Vue3 / Angular 多技术栈,领有主题配置零碎 / 中后盾模板 / CLI 命令行等效率晋升工具,可帮忙开发者高效开发 Web 利用。

外围亮点:

  1. 跨端跨框架:应用 Renderless 无渲染组件设计架构,实现了一套代码同时反对 Vue2 / Vue3,PC / Mobile 端,并反对函数级别的逻辑定制和全模板替换,灵活性好、二次开发能力强。
  2. 组件丰盛:PC 端有100+组件,挪动端有30+组件,蕴含高频组件 Table、Tree、Select 等,内置虚构滚动,保障大数据场景下的晦涩体验,除了业界常见组件之外,咱们还提供了一些独有的特色组件,如:Split 面板分割器、IpAddress IP地址输入框、Calendar 日历、Crop 图片裁切等
  3. 配置式组件:组件反对模板式和配置式两种应用形式,适宜低代码平台,目前团队曾经将 OpenTiny 集成到外部的低代码平台,针对低码平台做了大量优化
  4. 周边生态齐全:提供了基于 Angular + TypeScript 的 TinyNG 组件库,提供蕴含 10+ 实用功能、20+ 典型页面的 TinyPro 中后盾模板,提供笼罩前端开发全流程的 TinyCLI 工程化工具,提供弱小的在线主题配置平台 TinyTheme

分割咱们:

  • 官网公众号:OpenTiny
  • OpenTiny 官网: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社区,B站/抖音/小红书/视频号。