译文:JS事件循环机制(event loop)之宏任务、微任务

译文:JS事件循环机制(event loop)之宏任务、微任务

原文标题:《Tasks, microtasks, queues and schedules》

这是一篇谷歌大神文章,写得很是精彩。译者想借此次翻译深刻学习一下,因为水平有限,英文好的同窗建议直接阅读原文。
原文地址:Tasks, microtasks, queues and schedules
下面正文开始:html

Tasks, microtasks, queues and schedules

首先看一段代码:html5

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
可是在不一样浏览器上的结果倒是让人懵逼的。es6

Microsoft Edge, Firefox 40, iOS Safari 和 desktop Safari 8.0.8在promise1,promise2以前打印了setTimeout,--虽然看起来像竞态条件。
但让人懵逼的是Firefox 39 , Safari 8.0.7会打印出正确顺序。
译者注:译者的Microsoft Edge 38.14393.2068.0,Firefox 59.0.2 版本会打印出正确顺序,应该已经支持了吧,其余浏览器未验证。web

为何会出现这样打印顺序呢?

要理解这些你首先须要对事件循环机制处理宏任务和微任务的方式有了解。
若是是第一次接触信息量会有点大。深呼吸……chrome

每一个线程都会有它本身的event loop(事件循环),因此都能独立运行。然而全部同源窗口会共享一个event loop以同步通讯。event loop会一直运行,来执行进入队列的宏任务。一个event loop有多种的宏任务源(译者注:event等等),这些宏任务源保证了在本任务源内的顺序。可是浏览器每次都会选择一个源中的一个宏任务去执行。这保证了浏览器给与一些宏任务(如用户输入)以更高的优先级。好的,跟着我继续……api

宏任务(task)

浏览器为了可以使得JS内部task与DOM任务可以有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行从新渲染 (task->渲染->task->...)
鼠标点击会触发一个事件回调,须要执行一个宏任务,而后解析HTMl。还有下面这个例子,setTimeoutpromise

setTimeout的做用是等待给定的时间后为它的回调产生一个新的宏任务。这就是为何打印‘setTimeout’在‘script end’以后。由于打印‘script end’是第一个宏任务里面的事情,而‘setTimeout’是另外一个独立的任务里面打印的。浏览器

微任务(Microtasks )

微任务一般来讲就是须要在当前 task 执行结束后当即执行的任务,好比对一系列动做作出反馈,或或者是须要异步的执行任务而又不须要分配一个新的 task,这样即可以减少一点性能的开销。只要执行栈中没有其余的js代码正在执行且每一个宏任务执行完,微任务队列会当即执行。若是在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,以后也会被执行。微任务包括了mutation observe的回调还有接下来的例子promise的回调。app

一旦一个pormise有告终果,或者早已有告终果(有告终果是指这个promise到了fulfilled或rejected状态),他就会为它的回调产生一个微任务,这就保证了回调异步的执行即便这个promise早已有告终果。因此对一个已经有告终果的promise调用.then(yey, nay)会当即产生一个微任务。这就是为何‘promise1’,'promise2'会打印在‘script end’以后,由于全部微任务执行的时候,当前执行栈的代码必须已经执行完毕。‘promise1’,'promise2'会打印在‘setTimeout’以前是由于全部微任务总会在下一个宏任务以前所有执行完毕。less

逐步执行demo:译者注,这里做者实现了一个相似于debug,逐步执行的demo,其中还加入了执行栈的动画还有讲解,建议你们去原文观看
原文

是的,我弄了一个逐步的图标。你怎么度过你的周六?和你的朋友出去享受阳光?emmmm,若是对我惊艳的ui交互设计看不懂,点击左右箭头试试吧。

那为何那些浏览器打印顺序不同咧?

有些浏览会会打印出:
script start, script end, setTimeout, promise1, promise2。
他们会在setTimeout以后执行promise的回调,就好像这些浏览器会把promise的回调视做一个新的宏任务而不是微任务。

其实无可厚非,由于promises 来自于ECMAScript 的标准而不是HTML标准。
ECMAScript 有个关于jobs的概念和微任务挺相似的,可是否明确具备关联关系却还没有定论(相关讨论)。然而,广泛的观点是promise应该属于微任务。

若是说把 promise 当作一个新的 task 来执行的话,这将会形成一些性能上的问题,由于 promise 的回调函数可能会被延迟执行,由于在每个 task 执行结束后浏览器可能会进行一些渲染工做。因为做为一个 task 将会和其余任务来源(task source)相互影响,这也会形成一些不肯定性,同时这也将打破一些与其余 API 的交互,这样一来便会形成一系列的问题。

这里有一个关于让Edge把promise加入微任务的提议,其实WebKit 早已悄悄正确实现。因此我猜Safari最终会修复,Firefox 43好像已修复。

如何分辨宏任务和微任务?

实际测试是一种方法,观察日志打印顺序与promise和setTimeout的关系,可是首先浏览器对这二者的实现要正确。
还有一个稳妥方法就是看文档,好比setTimeout是宏任务,mutation是微任务。
正如上文提到的,ECMAScript 中把微任务叫作jobs,EnqueueJob
是微任务。
接下来,让咱们看一些复杂的例子吧

一级boss战

写这篇文章前我就犯了这个错。来看代码

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

在看接下来的js代码,若是我点击div.inner会打印什么?

// 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
//监听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);

偷看答案前先试一试啊,tips:日志可能出现屡次哦。
结果以下:
click
promise
mutate
click
promise
mutate
timeout
timeout
你猜对了吗。你可能猜对了,可是许多浏览器却不这样以为。
图片描述

译者注:译者本机测试
Chrome( 64.0.3282.167(正式版本) (64 位))相同,
Edge(Edge 38.14393.2068.0)不一样(与Chrome顺序相同)
Firefox 32位 59.0.2

  • click
  • mutate
  • click
  • mutate
  • promise
  • promise
  • timeout
  • timeout

哪一个是对的?

分发click event是一个宏任务,Mutation observer和promise都会进入微任务队列,setTimeout回调是一个宏任务,因此来看demo
做者演示demo,建议原文观看demo
因此chrome是对的,我以前也不知道只要执行栈中没有js代码在执行,微任务会在回调后当即执行,我以前认为它只会在宏任务结束后执行(Although we are mid-task,microtasks are processed after callbacks if the stack is empty).这个规则来自于HTML标准中关于回调调用的部分

If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: Cleaning up after a callback step 3

若是js执行栈空了,当即执行microtask checkpoint
—— HTML: Cleaning up after a callback
microtask checkpoint 会检查整个微任务队列,除非正在执行这个检查动做。ECMAScript 标准中说到

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

HTML环境下,必须执行。

浏览器哪里出错了?

Firefox和Safari在click监听器回调之间正确执行了mutation 回调的微任务,但promise打印结果却出如今了错误的位置。
无可厚非的是jobs和微任务的关系太含糊不清,不过我仍认为应该在click监听器回调之间执行。
Edge咱们早就知道会把promise回调放进错误的队列,但他也也没在click监听器回调之间执行微任务队列,而是在全部监听器回调后执行,这打印click以后只打印了一次muteta,为此我给它提了个bug。

一级boss愤怒的大哥来了

用刚才的代码,若是咱们这样执行会发生什么。

inner.click();

这依旧会开始分发事件,但此次是使用脚本而不是交互点击。
click
click
promise
mutate
promise
timeout
timeout
图片描述

我发誓我从chrome获得的答案一直不同- -。我已经更新了这个表许许屡次了。我以为我是错误地测试了Canary。假如你在 Chrome 中获得了不一样的结果,请在评论中告诉我是哪一个版本。

为何不同呢?

来看demo发生了什么,原做者的演示demo
因此正确的顺序是click, click, promise, mutate, promise, timeout, timeout,看来chrome是对的。
在每一个监听器回调调用以后

If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: Cleaning up after a callback step 3

以前的例子,微任务会在监听器回调之间执行。但这里的例子,click()会致使事件同步分发,因此在监听器回调之间Js执行栈不为空,而上述的这个规则保证了微任务不会打断正在执行的js.这意味着咱们不能在监听器回调之间执行微任务,微任务会在监听器以后执行。

这能影响到什么?

译者注:对IndexedDB 理解不深刻,这段就不翻译了- -
Yeah, it'll bite you in obscure places (ouch). I encountered this while trying to create a simple wrapper library for IndexedDB that uses promises rather than weird IDBRequest objects. It almost makes IDB fun to use.

When IDB fires a success event, the related transaction object becomes inactive after dispatching (step 4). If I create a promise that resolves when this event fires, the callbacks should run before step 4 while the transaction is still active, but that doesn't happen in browsers other than Chrome, rendering the library kinda useless.

You can actually work around this problem in Firefox, because promise polyfills such as es6-promise use mutation observers for callbacks, which correctly use microtasks. Safari seems to suffer from race conditions with that fix, but that could just be their broken implementation of IDB. Unfortunately, things consistently fail in IE/Edge, as mutation events aren't handled after callbacks.

Hopefully we'll start to see some interoperability here soon.

干得不错!

总结一下:

  • 宏任务按顺序执行,且浏览器在每一个宏任务之间渲染页面
  • 全部微任务也按顺序执行,且在如下场景会当即执行全部微任务

    • 每一个回调以后且js执行栈中为空。
    • 每一个宏任务结束后。

但愿你已经熟悉了eventloop.

相关文章
相关标签/搜索