参考文档

任务队列和微任务队列的区别很简单,但却很重要:

  • 当执行来自任务队列中的任务时,在每一次新的事件循环开始迭代的时候运行时都会执行队列中的每个任务。在每次迭代开始之后加入到队列中的任务需要在下一次迭代开始之后才会被执行。
  • 每次当一个任务退出且执行上下文为空的时候,微任务队列中的每一个微任务会依次被执行。不同的是它会等到微任务队列为空才会停止执行——即使中途有微任务加入。换句话说,微任务可以添加新的微任务到队列中,并在下一个任务开始执行之前且当前事件循环结束之前执行完所有的微任务。

任务(Tasks)

一个 任务 就是由执行诸如从头执行一段程序、执行一个事件回调或一个 interval/timeout 被触发之类的标准机制而被调度的任意 JavaScript 代码

在以下时机,任务会被添加到任务队列:

  • 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个 <script> 元素中运行代码)。
  • 触发了一个事件,将其回调函数添加到任务队列时。
  • 执行到一个由 setTimeout() 或 setInterval() 创建的 timeout 或 interval,以致相应的回调函数被添加到任务队列时。

事件循环驱动你的代码按照这些任务排队的顺序,一个接一个地处理它们。在当前迭代轮次中,只有那些当事件循环过程开始时已经处于任务队列中的任务会被执行。其余的任务不得不等待到下一次迭代。

微任务(Microtasks)

起初微任务和任务之间的差异看起来不大。它们很相似;都由位于某个队列的 JavaScript 代码组成并在合适的时候运行。但是,只有在迭代开始时队列中存在的任务才会被事件循环一个接一个地运行,这和处理微任务队列是殊为不同的。

有两点关键的区别。

首先,每当一个任务存在,事件循环都会检查该任务是否正把控制权交给其他 JavaScript 代码。如若不然,事件循环就会运行微任务队列中的所有微任务。接下来微任务循环会在事件循环的每次迭代中被处理多次,包括处理完事件和其他回调之后。

其次,如果一个微任务通过调用 queueMicrotask(), 向队列中加入了更多的微任务,则那些新加入的微任务会早于下一个任务运行。这是因为事件循环会持续调用微任务直至队列中没有留存的,即使是在有更多微任务持续被加入的情况下。

入列微任务

就其本身而言,应该使用微任务的典型情况,要么只有在没有其他办法的时候,要么是当创建框架或库时需要使用微任务达成其功能。虽然在过去要使得入列微任务成为可能有可用的技巧(比如创建一个立即 resolve 的 promise),但新加入的 queueMicrotask() 方法增加了一种标准的方式,可以安全的引入微任务而避免使用额外的技巧。

通过引入 queueMicrotask(),由晦涩地使用 promise 去创建微任务而带来的风险就可以被避免了。举例来说,当使用 promise 创建微任务时,由回调抛出的异常被报告为 rejected promises 而不是标准异常。同时,创建和销毁 promise 带来了事件和内存方面的额外开销,这是正确入列微任务的函数应该避免的。

简单的传入一个 JavaScript Function ,以在 queueMicrotask() 方法中处理微任务时供其上下文调用即可;取决于当前执行上下文, queueMicrotask() 以定义的形式被暴露在 Window 或 Worker 接口上。

1
2
3
queueMicrotask(() => {
/* 微任务中将运行的代码 */
});

微任务函数本身没有参数,也不返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setTimeout(() => {
console.log(5)
}, 0)
new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
}).then(() => {
console.log(4)
})
console.log(3)

---

# Output
1 2 3 4 5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
}).then(() => {
console.log(4)
})

等价于

{
console.log(1);
queueMicrotask(() => {
console.log(4)
});
console.log(2);
}