## 1 引言

本周跟着 [Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) 这篇文章一起深入理解这些概念间的区别。

先说结论：

- Tasks 按顺序执行，浏览器可能在 Tasks 之间执行渲染。
- Microtasks 也按顺序执行，时机是：
  - 如果没有执行中的 js 堆栈，则在每个回调之后。
  - 在每个 task 之后。

## 2 概述

### Event Loop

在说这些概念前，先要介绍 Event Loop。

首先浏览器是多线程的，每个 JS 脚本都在单线程中执行，每个线程都有自己的 Event Loop，同源的所有浏览器窗口共享一个 Event Loop 以便通信。

Event Loop 会持续循环的执行所有排队中的任务，浏览器会为这些任务划分优先级，按照优先级来执行，这就会导致 Tasks 与 Microtasks 执行顺序与调用顺序的不同。

### promise 与 setTimeout

看下面代码的输出顺序：

```js
console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

Promise.resolve()
  .then(function () {
    console.log("promise1");
  })
  .then(function () {
    console.log("promise2");
  });

console.log("script end");
```

正确答案是 `script start`, `script end`, `promise1`, `promise2`, `setTimeout`，在线程中，同步脚本执行优先级最高，然后 promise 任务会存放到 Microtasks，setTimeout 任务会存放到 Tasks，Microtasks 会优先于 Tasks 执行。

Microtasks 中文可以翻译为微任务，只要有 Microtasks 插入，就会不断执行 Microtasks 队列直到结束，在结束前都不会执行到 Tasks。

### 点击冒泡 + 任务

下面给出了更复杂的例子，提前说明后面的例子 Chrome、Firefox、Safari、Edge 浏览器的结果完全不一样，但只有 Chrome 的运行结果是对的！为什么 Chrome 是对的呢，请看下面的分析：

```html
<div class="outer">
  <div class="inner"></div>
</div>
```

```js
// Let's get hold of those elements
var outer = document.querySelector(".outer");
var inner = document.querySelector(".inner");

// Let's listen for attribute changes on the
// outer element
new MutationObserver(function () {
  console.log("mutate");
}).observe(outer, {
  attributes: true,
});

// Here's a click listener…
function onClick() {
  console.log("click");

  setTimeout(function () {
    console.log("timeout");
  }, 0);

  Promise.resolve().then(function () {
    console.log("promise");
  });

  outer.setAttribute("data-random", Math.random());
}

// …which we'll attach to both elements
inner.addEventListener("click", onClick);
outer.addEventListener("click", onClick);
```

点击 `inner` 区块后，正确输出顺序应该是：

```text
click
promise
mutate
click
promise
mutate
timeout
timeout
```

逻辑如下：

1. 点击触发 `onClick` 函数入栈。
2. 立即执行 `console.log('click')` 打印 `click`。
3. `console.log('timeout')` 入栈 Tasks。
4. `console.log('promise')` 入栈 microtasks。
5. `outer.setAttribute('data-random')` 的触发导致监听者 `MutationObserver` 入栈 microtasks。
6. `onClick` 函数执行完毕，此时线程调用栈为空，开始执行 microtasks 队列。
7. 打印 `promise`，打印 `mutate`，此时 microtasks 已空。
8. 执行冒泡机制，outer div 也触发 `onClick` 函数，同理，打印 `promise`，打印 `mutate`。
9. 都执行完后，执行 Tasks，打印 `timeout`，打印 `timeout`。

### 模拟点击冒泡 + 任务

如果将触发 `onClick` 行为由点击改为：

```js
inner.click();
```

结果会不同吗？答案是会（单元测试与用户行为不符合，单测也有无解的时候）。然而四大浏览器的执行结果也是完全不一样，但从逻辑上讲仍然 Chrome 是对的，让我们看下 Chrome 的结果：

```text
click
click
promise
mutate
promise
timeout
timeout
```

逻辑如下：

1. `inner.click()` 触发 `onClick` 函数入栈。
2. 立即执行 `console.log('click')` 打印 `click`。
3. `console.log('timeout')` 入栈 Tasks。
4. `console.log('promise')` 入栈 microtasks。
5. `outer.setAttribute('data-random')` 的触发导致监听者 `MutationObserver` 入栈 microtasks。
6. 由于冒泡改为 js 调用栈执行，所以此时 js 调用栈未结束，不会执行 microtasks，反而是继续执行冒泡，outer 的 `onClick` 函数入栈。
7. 立即执行 `console.log('click')` 打印 `click`。
8. `console.log('timeout')` 入栈 Tasks。
9. `console.log('promise')` 入栈 microtasks。
10. `MutationObserver` 由于还没调用，因此这次 `outer.setAttribute('data-random')` 的改动实际上没有作用。
11. js 调用栈执行完毕，开始执行 microtasks，按照入栈顺序，打印 `promise`，`mutate`，`promise`。
12. microtasks 执行完毕，开始执行 Tasks，打印 `timeout`，`timeout`。

## 3 精读

基于任务调度这么复杂，且浏览器实现方式很不同，下面两件事是我很不推荐的：

1. 业务逻辑 “巧妙” 依赖了 microtasks 与 Tasks 执行逻辑的微妙差异。
2. 死记硬背调用顺序。

且不说依赖了调用顺序的业务逻辑本身就很难维护，不同浏览器之间对任务调用顺序还是不同的，这可能源于对 W3C 标准规范理解的偏差，也可能是 BUG，这会导致依赖于此的逻辑非常脆弱。

虽然上面两个例子非常复杂，但我们也不必把这个例子当作经典背诵，只要记住文章开头提到的执行逻辑就可以推导：

- Tasks 按顺序执行，浏览器可能在 Tasks 之间执行渲染。
- Microtasks 也按顺序执行，时机是：
  - 如果没有执行中的 js 堆栈，则在每个回调之后。
  - 在每个 task 之后。

记住 `Promise` 是 `Microtasks`，`setTimeout` 是 `Tasks`，JS 一次 Event Loop 完毕后，即调用栈没有内容时才会执行 `Microtasks` -> `Tasks`，在执行 `Microtasks` 过程中插入的 `Microtasks` 会按顺序继续执行，而执行 `Tasks` 中插入的 `Microtasks` 得等到调用栈执行完后才继续执行。

上面说的内容都是指一次 Event Loop 时立即执行的优先级，不要和执行延迟时间弄混淆了。

把 JS 线程的 Event Loop 当作一个函数，函数内同步逻辑执行优先级是最高的，如果遇到 `Microtasks` 或 `Tasks` 就会立即记录下来，当一次 Event Loop 执行完后立即调用 `Microtasks`，等 `Microtasks` 队列执行完毕后可能进行一些渲染行为，等这些浏览器操作完成后，再考虑执行 `Tasks` 队列。

## 4 总结

最后，还是要强调一句，不要依赖 `Microtasks` 与 `Tasks` 的执行顺序，尤其在申明式编程环境中，我们可以把 `Microtasks` 与 `Tasks` 都当作是异步内容，在渲染时做好状态判断即可，不用关心先后顺序。

> 讨论地址是：[精读《Tasks, microtasks, queues and schedules》· Issue #264 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/264)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
