到目前为止,我们已经讨论过了 zone 如何创建,fork,并且如何在异步操作之间维持状态。zone 和执行的栈帧在调用栈中顺序相同(但是一般深度更低)。在调用栈中有一个 zone 是特殊的,就是最顶部的 zone。当这个 zone 出现的时候,语句执行从 JavaScript 转移到了原生环境。(用另一种话说,就是当最顶部的执行栈帧出现的时候,控制权返回了原生代码手上。)我们把最顶部的 zone 称作任务。

  1. at someOtherFn()[zoneB];
  2. at Zone.prototype.run()[zoneA -> zoneB]
  3. at someFn()[zoneA];
  4. at Zone.prototype.run()[<root> -> zoneA] <<< 任务帧 / Zone

任务是特殊的是因为知道任务何时退出对于下述情况很有用:

  • 框架知道何时渲染 UI
  • 测量任务的 进入/离开 时间可以知道总的 脚本/任务 执行时间。
  • 退出返回执行到原生代码允许渲染或者 I/O 操作。

(知道何时发生可以允许变换操作,比如在渲染或者 I/O 操作前做些什么)

知道任务 进入/离开 的时间就是知道一个 API 调用应该在何时创建一个 将要/可能要 执行的任务。

  • 测试框架可以通过在任务创建时抛出错误来强制执行同步测试
  • 测试框架可以自动检测异步任务并且通过等待的所有任务执行完毕来等待异步任务的完成。
  • 通过追踪任务创建实现长堆栈追踪
  • 通过追踪用户触发的异步任务来追踪用户感知的操作。

有三种不同的任务:

  1. 微任务:微任务会在调用栈为空时立即执行。微任务一定会在宿主环境进行 I/O 操作和渲染操作之前进行。宏任务和事件任务只会在微任务队列被执行完毕之后才会被执行。(比如 Promise.then() 就是在微任务中执行的)
  2. 宏任务:宏任务会在 I/O 操作和渲染操作间执行。(比如 setTimeout,setInterval ,等等)宏任务一定会执行至少一次或者被取消(有时候可能会重复执行,比如 setInterval)。宏任务会按照默认顺序执行。
  3. 事件任务:事件任务和宏任务很类似,但是不想宏任务,事件任务可能永远不会被执行。当事件任务执行的时候,他会抢在宏任务队列之前执行。事件任务不会创建一个队列。(比如用户触发的 click , mousemove 时间 或者 XHR 状态事件。)


类型 创建 执行
微任务 微任务在 Promise 调用 then 时创建 then 方法的回调在微任务内执行。微任务一旦创建就无法取消并且确定会并且只会执行一次。
宏任务 宏任务在用户代码调用 setTimeout setInterval 这种 API 时创建 在渲染和 I/O 操作之后,回调在宏任务内执行。一旦宏任务执行完毕,在渲染操作和 I/O 操作执行之前,会首先执行完微任务队列。
事件任务 事件任务使用 addEventListener 或者类似机制的方法时创建 事件任务可能永远不会执行,但也有可能在任何时候执行,并且次数不确定,也不可能预测。

为什么需要知道这些:

  • 知道任务何时执行以及微任务队列何时清空可以让框架知道什么时候渲染 UI
  • 强制禁止任务创建可以让测试框架确定测试是同步的(可以更快和更连贯)
  • 追踪所有任务的执行完成度可以让测试框架知道异步方法已经执行完毕。
  • 追踪用户触发的操作并且等待任务完成可以允许应用程序追踪用户操作触发的后续动作。
  • 追踪所有任务的执行完成度可以让端到端测试框架知道在判断输出和继续下一步之前需要等待多久(这可以让端对端测试更快和更连贯)

译注: zonejs 使用猴子补丁的方法重写了 js 的所有异步方法,并将其分为三类

  1. 微任务:与 js 默认的微任务定义一致
  2. 宏任务:与 js 默认的宏任务定义一致
  3. 事件任务:所有事件。

使用者可以对某一类型的异步操作统一处理。

另一种思考任务必要性的方式是如果没有任务,下述行为都不可能实行。

第一节:任务创建

为了追踪任务,我们重温以下 Zone 是如何通过使用猴子补丁重写 setTimeout 来追踪的。

  1. // 保存原生的 setTimeout 的引用
  2. let originalSetTimeout = window.setTimeout;
  3. // 通过在 zone 中包装回调来重写 API
  4. window.setTimeout = function(callback, delay) {
  5. // 在当前 zone 中使用 scheduleTask Api
  6. Zone.current.scheduleMacroTask(
  7. // 调试信息
  8. 'setTimeout',
  9. // 需要在当前 zone 中执行的回调
  10. callback,
  11. // 可选信息
  12. null,
  13. // 默认调用行为
  14. (task) => {
  15. return originalSetTimeout(
  16. // 使用 task.invoke 方法,这样 task 就能在正确的 zone 中执行回调
  17. task.invoke,
  18. // 原延迟值
  19. delay
  20. );
  21. });
  22. }

使用范例:

  1. // 创建一个日志 zone
  2. let logZone = Zone.current.fork({
  3. onScheduleTask: function(parentZoneDelegate, currentZone,
  4. targetZone, task) {
  5. // 当异步任务建立的时候打印
  6. console.log('Schedule', task.source);
  7. return parentZoneDelegate.scheduleTask(targetZone, task);
  8. },
  9. onInvokeTask: function(parentZoneDelegate, currentZone,
  10. targetZone, task, applyThis, applyArgs) {
  11. // 当异步任务调用的时候打印
  12. console.log('Invoke', task.source);
  13. return parentZoneDelegate.invokeTask(
  14. targetZone, task, applyThis, applyArgs);
  15. }
  16. });
  17. console.log('start');
  18. logZone.run(() => {
  19. setTimeout(() => null, 0);
  20. });
  21. console.log('end');

输出结果:

  1. start
  2. Schedule setTimeout
  3. end
  4. Invoke setTimeout

重点:

  • 所有建立任务的 API 都使用 Zone.prototype.scheduleTask() 取代了 Zone.prototype.wrap() 方法
  • 任务使用 Zone.prototype.runGuarded() 方法调用回调函数,这样就能捕获所有错误。
  • 就像 zone 使用 invoke() 方法一样,任务使用 invokeTask() 方法来重写任务执行方法。


第二节:任务追踪

保持对任务的追踪对了解资源何时可以被释放或者虚拟机该何时进行下一步操作是很有用处的。(所有的微任务都已经执行完毕并且渲染和 I/O 操作会在随后进行。)