10

精读《Tasks, microtasks, queues and schedules》

 3 years ago
source link: https://zhuanlan.zhihu.com/p/187069497
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

精读《Tasks, microtasks, queues and schedules》

前端开发话题下的优秀回答者

1 引言

本周跟着 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

看下面代码的输出顺序:

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 是对的呢,请看下面的分析:

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

// 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 区块后,正确输出顺序应该是:

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 行为由点击改为:

inner.click();

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

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,按照入栈顺序,打印 promisemutatepromise
  12. microtasks 执行完毕,开始执行 Tasks,打印 timeouttimeout

3 精读

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

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

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

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

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

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

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

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

4 总结

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

讨论地址是:精读《Tasks, microtasks, queues and schedules》· Issue #264 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK