到目前为止,我们已经讨论了 zone 如何创建、分叉以及它们如何在异步操作中维护它们的状态。 区域在运行时嵌套的方式与嵌套执行堆栈帧的方式相同(但通常具有较低的深度)。在嵌套的上下文中,有一个区域是特殊的,它是最顶部的区域。 当该区域存在时,执行从 JavaScript 转换到本机主机环境。 (另一种说法是,当最顶端的执行堆栈帧退出时,控制权返回给本机代码。)我们将最顶端的区域称为任务。
任务很特别,因为在这些情况下知道任务何时即将退出很有用:
- 框架知道何时渲染 UI。
- 测量进入/离开允许了解总脚本/任务时间。
- 退出将执行返回到本机代码允许渲染或执行 I/O 操作。 (知道什么时候即将发生允许事务行为,即在渲染和/或 I/O 之前做事。)
了解任务何时进入/离开的必然结果是了解 API 调用何时创建将/可能在未来执行的任务。
- 测试框架可以通过创建任务来强制执行同步测试。
- 测试框架可以自动检测异步测试并通过等待所有计划任务执行来等待其完成。
- 通过跟踪任务调度,跨越异步边界的长堆栈跟踪。
- 通过跟踪用户触发的异步任务何时完成来跟踪用户感知的操作。
共有三种感兴趣的任务:
- 微任务:微任务是在空堆栈帧上尽快执行的工作。 微任务保证在宿主环境执行渲染或 I/O 操作之前运行。 在另一个 MacroTask 或 EventTask 运行之前,微任务队列必须为空。 (即 Promise.then() 在微任务中执行)
- MacroTask:宏任务与宿主环境的渲染和 I/O 操作交错进行。 (即setTimeout、setInterval 等。)宏任务保证至少运行一次或取消(有些可以重复运行,例如setInterval)。 宏任务具有隐含的执行顺序。
- EventTask:事件任务类似于宏任务,但与宏任务不同,它们可能永远不会运行。 当一个 EventTask 运行时,它会抢占下一个任务是宏任务队列。 事件任务不创建队列。 (即用户单击、鼠标移动、XHR 状态更改。)
Type | Scheduled | Execution |
---|---|---|
MicroTask | 微任务在需要调用 thenCallback.promise.then(thenCallback) 时由promisses 调度。 | thenCallback 的执行在一个微任务中运行。 一旦微任务被调度,它就不能被取消,并且保证它只运行一次。 |
MacroTask | 宏任务由用户代码使用显式 API 进行调度,例如 setTimeout(callback)、setInterval(callback) 等。 | 在任何渲染和 I/O 操作完成后,回调的执行在宏任务中运行。 一旦宏任务完成,微任务队列就会在执行被传递到主机环境以进行更多渲染和 I/O 操作之前排空。 |
EventTask | 事件任务使用 addEventListener(‘click’, eventCallback) 或类似机制进行调度。 | 事件任务的执行可能永远不会发生,发生在不可预测的时间,并且可能发生不止一次,因此无法知道它将执行多少次。 |
为什么这很有用:
- 知道任务何时执行并且微任务队列为空允许框架知道何时渲染 UI。
- 强制执行不安排任何任务允许测试框架确保测试是同步的(因此快速且非不稳定)。
- 跟踪所有计划任务何时执行允许测试框架知道异步测试何时完成。
- 跟踪源自用户操作并等待所有计划任务执行的任务允许应用程序跟踪该操作的感知用户延迟。
- 知道所有未完成的任务何时执行允许端到端测试框架知道在断言期望和继续下一步之前要等待多长时间。 (这使得端到端测试更快,并减少了片状。)
另一种思考任务需求的方式是,如果没有任务,上述行为是不可能实现的。