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 elementsvar outer = document.querySelector(".outer");var inner = document.querySelector(".inner");// Let's listen for attribute changes on the// outer elementnew 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 elementsinner.addEventListener("click", onClick);outer.addEventListener("click", onClick);
点击 inner
区块后,正确输入程序应该是:
click
promise
mutate
click
promise
mutate
timeout
timeout
逻辑如下:
- 点击触发
onClick
函数入栈。 - 立刻执行
console.log('click')
打印click
。 console.log('timeout')
入栈 Tasks。console.log('promise')
入栈 microtasks。outer.setAttribute('data-random')
的触发导致监听者MutationObserver
入栈 microtasks。onClick
函数执行结束,此时线程调用栈为空,开始执行 microtasks 队列。- 打印
promise
,打印mutate
,此时 microtasks 已空。 - 执行冒泡机制,outer div 也触发
onClick
函数,同理,打印promise
,打印mutate
。 - 都执行完后,执行 Tasks,打印
timeout
,打印timeout
。
模仿点击冒泡 + 工作
如果将触发 onClick
行为由点击改为:
inner.click();
后果会不同吗?答案是会(单元测试与用户行为不合乎,单测也有无解的时候)。然而四大浏览器的执行后果也是齐全不一样,但从逻辑上讲依然 Chrome 是对的,让咱们看下 Chrome 的后果:
click
click
promise
mutate
promise
timeout
timeout
逻辑如下:
inner.click()
触发onClick
函数入栈。- 立刻执行
console.log('click')
打印click
。 console.log('timeout')
入栈 Tasks。console.log('promise')
入栈 microtasks。outer.setAttribute('data-random')
的触发导致监听者MutationObserver
入栈 microtasks。- 因为冒泡改为 js 调用栈执行,所以此时 js 调用栈未完结,不会执行 microtasks,反而是继续执行冒泡,outer 的
onClick
函数入栈。 - 立刻执行
console.log('click')
打印click
。 console.log('timeout')
入栈 Tasks。console.log('promise')
入栈 microtasks。MutationObserver
因为还没调用,因而这次outer.setAttribute('data-random')
的改变实际上没有作用。- js 调用栈执行结束,开始执行 microtasks,依照入栈程序,打印
promise
,mutate
,promise
。 - microtasks 执行结束,开始执行 Tasks,打印
timeout
,timeout
。
3 精读
基于任务调度这么简单,且浏览器实现形式很不同,上面两件事是我很不举荐的:
- 业务逻辑“奇妙”依赖了 microtasks 与 Tasks 执行逻辑的奥妙差别。
- 死记硬背调用程序。
且不说依赖了调用程序的业务逻辑自身就很难保护,不同浏览器之间对工作调用程序还是不同的,这可能源于对 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
如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)