[译] 深入理解 JS 事件循环(二)- task and microtask

关注“重度前端”

助力前端深度学习

━━━━━

前言

读了这篇文章后相信对event loop机制了解就比较全面了,增加我们的编程内功。

microtask 这一名词是 JS 中比较新的概念,几乎所有人都是在学习 ES6 的 Promise 时才接触这一新概念,我也不例外。当我刚开始学习 Promise 的时候,对其中回调函数的执行方式特别着迷,于是乎便看到了 microtask 这一个单词,但是困难的是国内很少有关于这方面的文章,有一小部分人探讨过不过对其中的原理和机制的讲解也是十分晦涩难懂。直到我看到了 Jake Archibald 的文章,我才对 microtask 有了一个完整的认识,所以我便想把这篇文章翻译过来,供大家学习和参考。

  

    本篇文章绝大部分翻译自 Jake Archibald 的文章 Tasks, microtasks, queues and schedules。有英文功底的同学建议阅读原著,毕竟人家比我写的好...

  适合人群:有一定的 JavaScript 开发基础,对 JavaScript Event Loop 有基本的认识,掌握 ES6 Promise 。

初识 microtask

  让我们先来看一段代码,猜猜它将会以何种顺序输出:

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'

以上结果是不是和你想的不大一样,但这确实是正确答案。但是不同的浏览器可能会出现不同的输出顺序。

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 开始之前。

640?wx_fmt=png

浏览器兼容性

 有一些浏览器会输出:'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>
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

// 给 outer 添加一个观察者
new MutationObserver(function() {
  console.log('mutate');
}).observe(outer, {
  attributes: true
});

// click 回调函数
function onClick() {
  console.log('click');

  setTimeout(function() {
    console.log('timeout');
  }, 0);

  Promise.resolve().then(function() {
    console.log('promise');
  });

  outer.setAttribute('data-random', Math.random());
}

inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

执行结果猜对了吗 不过在这里不同的浏览器可能会有不同的结果。

ChromeFireFoxSafariEdge
clickclick  clickclick
promisemutatemutateclick
mutateclickclickmutate
clickmutatemutatetimeout
promisetimeoutpromisepromise
mutatepromisepromisetimeout
timeoutpromisetimeoutpromise
timeouttimeouttimeout

正确答案?

 click 的回调函数是一个 task,而 Promise 和 MutationObserver 是一microtask,setTimeout 是一个 task,我们可以看出,Chrome 给出的是正确答案。可以简单得出一个结论:用户操作的回调函数也是一个 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 进行一下总结:

microtask 和 task 一样严格按照时间先后顺序执行。

microtask 类型的任务包括 Promise callback 和 Mutation callback。

当 JS 执行栈为空时,便生成一个 microtask 检查点。

  JS 的 Event Loop 一直以来都是一个比较重要的部分,虽然在学完了过后一下子感觉不出有什么具体的卵用...但是,一旦 Event Loop 的运行机制印入了你的脑海里之后,对你的编程能力和程序设计能力的提高是帮助很大的。关于 Event Loop 的知识很少有相关的书籍有写到,一是因为这一块比较晦涩难懂,短时间内无法领略其精髓,二是因为具体能力提升不明显,不如认识几个 API 来的快,但是这却是我们编程的内力,他能在潜意识中左右着我们编程时思考问题的方式。

关于本文

原文地址:http://www.cnblogs.com/dong-xu/p/7000139.html

640?wx_fmt=png

 重度前端--助力深度学习

为web前端同行提供有价值、有深度的技术文章

官网:http://bigerfe.com

640?wx_fmt=jpeg

长按二维码关注我

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值