关于前端:zonejs由入门到放弃之二zonejs-API大练兵

28次阅读

共计 13760 个字符,预计需要花费 35 分钟才能阅读完成。

这是来自孙啸达 同学的 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 上下文中执行函数。

// 创立子 zone
const 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 还会不会执行。

// 创立子 zone
const 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 站 / 抖音 / 小红书 / 视频号。

正文完
 0