## 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
``` ```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),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。** > 关注 **前端精读微信公众号** > 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))