[译] 深入了解 JavaScript 事件循环(二)— task and microtask

引言

  microtask 这同叫作词是 JS 中于新的定义,几乎有人都是当上 ES6 的
Promise 时才接触这同新定义,我呢非异。当自身刚刚开念 Promise
的上,对里面回调函数的尽办法特别在迷,于是乎就看到了 microtask
这一个单词,但是困难的凡国内很少出关于这面的篇章,有一样稍部分人追究过但是针对中间的法则同建制的上课也是可怜涩难知晓。直到自己看来了
Jake Archibald 的章,我才对 microtask
有了一个完整的认,所以自己便想把立即篇稿子翻译过来,供大家学习与参照。

  本篇文章绝大部分翻译自 Jake Archibald 的章 Tasks, microtasks,
queues and
schedules。有英文基础的同校提议看原著,毕竟人家比较自己形容的好…

  适合人群:有自然之 JavaScript 开发基础,对 JavaScript Event Loop
有中心的认,掌握 ES6 Promise 。

初识 Microtask

  让咱们先来拘禁同样段代码,猜猜它将会为何种顺序输出:

 1 console.log('script start');
 2 
 3 setTimeout(function() {
 4   console.log('setTimeout');
 5 }, 0);
 6 
 7 Promise.resolve().then(function() {
 8   console.log('promise1');
 9 }).then(function() {
10   console.log('promise2');
11 });
12 
13 console.log('script end');

  你得在此处翻输出结果:

   正确的答案是:’script
start’、’script
end’、’promise1’、’promise2’、’setTimeout’。但是差之浏览器可能会见面世不同之出口顺序。

  Microsoft Edge, FireFox 40, iOS Safari 以及 Safari 8.0.8 将会以
‘promise1’ 和 ‘promise2’ 之前输出 ‘setTimeout’。但是奇怪的是,FireFox 39 跟 Safari
8.0.7 却以是遵循对的逐条输出。

 为什么?

  要掌握地方代码的出口原理,你不怕待了解
JavaScript 的 event loop 是怎么处理 tasks 以及
microtasks,当您首先次探望就无异积聚概念的时段,相信您吗是和自身同样的一头雾水,别急,让咱们事先充分呼吸一下,然后开始我们的
microtask 之一起。

  每一个“线程”都出一个单身的 event
loop,每一个 web worker 也闹一个独自的 event
loop,所以它们好单独的运转。如果非是这样的话,那么所有的窗口还拿共享一个
event loop,即使它们可以同步的通信。event loop
将会见不断不断的,有序的实行队列中之职责(tasks)。每一个 event loop
都发出正群不一的职责来(task
source),这些 task source 能够确保内部的
task 能够有序的履(参见标准 Indexed Database API
2.0)。不过,在各一样轮事件循环结束以后,浏览器可自动选择用哪一个
source 当中的 task
加入到执行队列中。这样呢即令了浏览器可事先挑选那些敏感性的职责,例如用户的之输入。(看了这段话,估计大部分人数还头晕了,别急…
be patient)

  Task
是严格依照时间顺序压栈和推行之,所以浏览器会使得 JavaScript 内部任务与
DOM 任务能够有序的履行。当一个 task 执行了晚,在生一个 task
执行起来前,浏览器可本着页面进行再次渲染。每一个 task
都是要分配的,例如从用户之点击操作及一个点击事件,渲染HTML文档,同时还有地方例子中的
setTimeout。

  setTimeout
的行事规律相信大家应该都了解,其中的推并无是全然可靠的,这是以 setTimeout
它会以延迟时间结束后分配一个初的 task 至 event loop
中,而非是立即实施,所以 setTimeout
的回调函数会等待前方的 task 都施行了晚再度运行。这即是怎 ‘setTimeout’ 会输出在 ‘script end’ 之后,因为 ‘script end’ 是率先单 task
的中间一些,而 ‘setTimeout’ 则是一个新的
task。这里我们先行说明了 event loop
的基本原理,接下我们会透过之来上课 microtask
的干活原理。

  Microtask 通常来说即使是待以此时此刻 task
执行完毕后就实施之职责,例如需要针对相同密密麻麻的任务做出回复,或者是需要异步的执行任务而而休欲分配一个初的
task,这样就是得以减一点特性的开销。microtask 任务队列是一个及 task
任务队列相互独立的行列,microtask 任务将见面于每一个 task
任务执行完毕之后执行。每一个 task 中生出的 microtask 都以会见补充加至
microtask 队列中,microtask 中起的 microtask
将会晤补加到当下行的尾部,并且 microtask
会按序的拍卖完班中之有着任务。microtask 类型的任务时包括了
MutationObserver 以及 Promise
的回调函数。

  每当一个 Promise
被决定(或是被拒绝),便会将该回调函数添加到 microtask
任务队列中作为一个初的 microtask 。这为确保了 Promise
可以异步的尽。所以当我们调用 .then(resolve, reject)
的时候,会立刻转变一个初的 microtask 添加至排中,这虽是怎上面的
‘promise1’ 和 ‘promise2’ 会输出在 ‘script end’ 之后,因为
microtask 任务队列中之职责要等眼前 task 执行完毕晚更实践,而 ‘promise1’ 和 ‘promise2’ 输出在 ‘setTimeout’ 之前,这是以
‘setTimeout’ 是一个初的
task,而 microtask 执行于脚下 task 结束后,下一个 task
开始之前。

  下面这 demo 将见面逐渐的分析 event loop
的运行方式:

   通过上述之 demo 相信大家对 microtask
的周转方式发生矣询问了咔嚓,不得不说自家异常倾 Jake Archibald
,人家自己一个许一个配之堆了一个事件轮循器出来。作为同样各类膜拜者,我为一个配一个配的堆了一个下!…详情可参见引言中贴发出之章。

 浏览器的兼容性

  有部分浏览器会输出:’script
start’、’script
end’、’setTimeout’、’promise1’、’promise2’。这些浏览器将会见于
‘setTimeout’ 之后输出
Promise 的回调函数,这看起像是马上类似浏览器不支持 microtask 而用 Promise
的回调函数作为一个新的 task 来实施。

  不了就一点吗是可解的,因为 Promise 是来源于于 ECMAScript 而无是
HTML。ES 当中有一个 “jobs” 的概念,它同 microtask
很一般,不过他们中的涉目前还未曾一个明了的概念。不过,普遍的共识都以为,Promise
的回调函数是应该作为一个 microtask 来运转的。

  如果说管 Promise 当做一个初的 task
来实施的话,这将会见导致局部特性达到的题材,因为 Promise
的回调函数可能会见吃延缓执行,因为于每一个 task
执行完毕后浏览器可能会见展开一些渲染工作。由于作为一个 task
将会见与其它职责来(task
source)相互影响,这吗会导致部分勿鲜明,同时立即也用打破一些以及任何 API
的互相,这样一来就会造成同密密麻麻的问题。

  Edge 浏览器目前早已修复了此问题(an Edge
ticket),WebKit
似乎总是明媒正娶的,Safari 终究也会见修复这个题材,在 FireFox 43
中此题目为早已受修复。

 如何判断 task 和 microtask

  直接测试输出是独雅好的主意,看看输出的逐条是再次如 Promise 还是再度像
setTimeout,趋向于 Promise 的则是 microtask,趋向于 setTimeout 的虽然是
task。

  还有雷同种植强烈的艺术是查标准。例如,timer-initialisation-steps 标准的第
16 步指出 “Queue the task task”。(注意原文中指出的凡 14
步,正确是相应是 16 步。)而
queue-a-mutation-record 标准的第
5 步指出 “Queue a mutation observer compound microtask”。

  同时需要小心的是,在 ES 当中称 microtask 为 “jobs”。比如
ES6标准 8.4节中的
“EnqueueJob” 意思指添加一个 microtask。

  现在,让我们来一个再扑朔迷离的例子…

 进阶 microtask

  在此之前,你得了解 MutationObserver 的以办法

1 <div class="outer">
2   <div class="inner"></div>
3 </div>

 1 var outer = document.querySelector('.outer');
 2 var inner = document.querySelector('.inner');
 3 
 4 // 给 outer 添加一个观察者
 5 new MutationObserver(function() {
 6   console.log('mutate');
 7 }).observe(outer, {
 8   attributes: true
 9 });
10 
11 // click 回调函数
12 function onClick() {
13   console.log('click');
14 
15   setTimeout(function() {
16     console.log('timeout');
17   }, 0);
18 
19   Promise.resolve().then(function() {
20     console.log('promise');
21   });
22 
23   outer.setAttribute('data-random', Math.random());
24 }
25 
26 inner.addEventListener('click', onClick);
27 outer.addEventListener('click', onClick);

  先试试着怀疑看程序用会晤怎么输出,你可于此间翻输出结果:

   猜对了也?不过以此地不同之浏览器可能会见生两样的结果。

Chrome FireFox Safari Edge
click click   click click
promise mutate mutate click
mutate click click mutate
click mutate mutate timeout
promise timeout promise promise
mutate promise promise timeout
timeout promise timeout promise
timeout timeout timeout  

 谁是不易答案?

   click 的回调函数是一个 task,而 Promise 和 MutationObserver 是一个
microtask,setTimeout 是一个 task,所以叫咱们一致步一步之来:

   通过上述 demo 我们可以看,Chrome
给闹之是毋庸置疑答案,这里发出好几暨事先 demo 不同之处在于,这里的 task
是一个回调函数而不是当下施行的剧本,所以我们得得出结论:用户操作的回调函数也是一个
task ,并且使一个 task 执行完毕还 JS stack 为空时,这时就检查
microtask ,如果无为空,则实施 microtask 队列。我们可瞻仰 HTML 标准:

If the stack of script settings
objects is
now empty, perform a microtask
checkpoint

— HTML: Cleaning up after a
callback step
3

 

Execution of a Job can be initiated only when there is no running
execution context and the execution context stack is empty…

— ECMAScript: Jobs and Job
Queues

  注意在 ES 当中称 microtask 为 jobs。

 为什么不同的浏览器表现各异?

  通过上面的事例可以测试出,FireFox 和 Safari 能够科学的施行 microtask
队列,这一点足通过 MutationObserver 的表现受到扣有,不过 Promise
被添加至事件队列中的不二法门接近有些不同。 这一点乎是能清楚的,由于 jobs
和 microtasks
的关系及概念时尚于模糊,不过人们还常见的盼望他们还能够在个别个事件监听器之间实行。这里有
FireFox 和
Safari 的 BUG
记录。(目前 Safari 已经修复了立无异于 BUG)

  以 Edge 中我们可以明确的顾那压入 Promise
的办法是不当的,同时其尽 microtask
队列的主意呢未科学,它没有在点滴只事件监听器之间实行,反而是以富有的轩然大波监听器之后执行,所以才会只有输出了同样坏
mutate 。Edge bug
ticket (目前既修复)

 驾驭 microtask

  到了这里,相信大家早就习得了 microtask
的运行机制了咔嚓,不过我们因而上述之事例再开一点点粗变化,比如我们运行一个:

1 inner.click();

  看看会来啊?

   同样,这里不同的浏览器表现也是匪等同的:

Chrome FireFox Safari  Edge 
click click   click click
click click click click
promise mutate mutate mutate
mutate timeout promise timeout
promise promise promise promise
timeout promise timeout timeout
timeout timeout timeout promise

  奇怪的是,在 Chrome
的独家版本里可能会见获不同的结果,究竟谁是不易答案?让咱一致步一步的分析:

   从点 demo 可以视,正确的答案应该是:’click’、’click’、’promise’、’mutate’、’promise’、’timeout’、’timeout’。所以看来 Chrome
给出之是科学答案。

  在前头一个 demo 中,microtask 将见面在片只 click
时间监听器之间运行,但是当这 demo 中,由于我们调用 .click() ,使得事件监听器的回调函数和当下运行的脚本同步实施要非是异步,所以时剧本的执行栈会一直压在 JS 执行栈
当中。所以在这个 demo 中 microtask 不见面在各级一个 click
事件后执行,而是在片独 click
事件实施到位之后执行。所以当此间我们得以还的对 microtask
的检查点进行定义:当执行栈(JS Stack)为空时,执行同一次 microtask
检查点。这也准保了任一个 task 还是一个 microtask
在实施完毕之后都见面转移一个 microtask 检查点,也保证了 microtask
队列能够一次性执行完毕。 

 总结

  关于 microtask
的任课就到此结束了,同学等发出没产生相同种渐入佳境的觉得啊?现在咱们来对
microtask 进行一下总结:

  •   microtask 和 task 一样严厉依照时间先后顺序执行。
  •   microtask 类型的任务包括 Promise callback 与 Mutation callback。
  •    当 JS 执行栈为空时,便格外成一个 microtask 检查点。

  JS 的 Event Loop
一直以来还是一个于重要之组成部分,虽然在模拟了了今后瞬间觉得不出有啊具体的卵用…但是,一旦
Event Loop
的运行机制印入了您的脑际里后,对而的编程能力及次设计能力的加强是扶特别酷之。关于
Event Loop
的知十分少来连锁的图书有描绘到,一凡是盖这等同片比较生硬难掌握,短日外无法了解其精华,二凡坐现实能力提升非鲜明,不如认识几个
API
来的不久,但是就可是咱们编程的内力,他能够当无形中中左右方我们编程时思考问题的法门。

  本文的 demo 都放在了 jsfiddle
上面,可任意转载(还是注明一下出处吧…)。