一直想对异步处理作一个研究,在查阅资料时发现了这篇文章,很是深刻的解释了事件循环中重的任务队列。原文中有代码执行工具,强烈建议本身执行一下查看结果,深刻体会task执行顺序。
建议看这篇译文以前先看这篇全面讲解事件循环的文章:https://mp.weixin.qq.com/s/vI...
翻译参考了这篇文章的部份内容:https://juejin.im/entry/55dbd...html
原文地址:Tasks, microtasks, queues and schedulesgit
当我告诉个人同事 Matt Gaunt 我想写一篇关于mircrotask、queueing和浏览器的Event Loop的文章。他说:“我实话跟你说吧,我是不会看的。” 好吧,不管如何我已经写完了,那么咱们坐下来一块儿看看,好吧?es6
若是你更喜欢视频,Philip Roberts 在 JSConf 上就事件循环有一个很棒的演讲——没有讲 microtasks,不过很好的介绍了其它概念。好,继续!github
思考下面 JavaScript 代码:web
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');
控制台上的输出顺序是怎样的呢?chrome
正确的答案是:windows
script start script end promise1 promise2 setTimeout
可是因为浏览器实现支持不一样致使结果也不一致。api
Microsoft Edge, Firefox 40, iOS Safari 及桌面 Safari 8.0.8 在 promise1 和 promise2 以前打印 setTimeout -- 这彷佛是浏览器厂商相互竞争致使的实现不一样。可是很奇怪的是,Firefox 39 和 Safari 8.0.7 居然结果都是对的(一致的)。promise
要想弄明白这些,你须要知道Event Loop是如何处理 tasks 和 microtasks的。若是你是第一次接触它,须要花些功夫才能弄明白。深呼吸。。。浏览器
每一个线程都有本身的事件循环,因此每一个 web worker 都有本身的事件循环,所以web worker才能够独立执行。而来自同域的全部窗口共享一个事件循环,因此它们能够同步地通讯。事件循环持续运行,直到清空 tasks 列队的任务。事件循环包括多种任务源,事件循环执行时会访问这些任务源,这样就肯定了各个任务源的执行顺序(IndexedDB 等规范定义了本身的任务源和执行顺序),但浏览器能够在每次循环中选择从哪一个任务源去执行一个任务。这容许浏览器优先考虑性能敏感的任务,例如用户输入。Ok ok, 留下来陪我坐会儿……
Tasks 被放到任务源中,浏览器内部执行转移到JavaScript/DOM领域,而且确保这些 tasks按序执行。在tasks执行期间,浏览器可能更新渲染。来自鼠标点击的事件回调须要安排一个task,解析HTML和setTimeout一样须要。
setTimeout延迟给定的时间,而后为它的回调安排一个新的task。这就是为何 setTimeout在 script end 以后打印:script end 在第一个task 内,setTimeout 在另外一个 task 内。好了,咱们快讲完了,剩下一点我须要大家坚持下……
Mircotasks队列一般用于存放一些任务,这些任务应该在正在执行的脚本以后当即执行,好比对一批动做做出反应,或者操做异步执行避免建立整个新任务形成的性能浪费。 只要没有其余JavaScript代码在执行中,而且在每一个task队列的任务结束时,microtask队列就会被处理。在处理 microtasks 队列期间,新添加到 microtasks 队列的任务也会被执行。 microtasks 包括 MutationObserver callbacks。例如上面的例子中的 promise的callback。
一个settled状态的promise(直接调用resolve或者reject)或者已经变成settled状态(异步请求被settled)的promise,会马上将它的callback(then)放到microtask队列里面。这就能保证promise的回调是异步的,即使promise已经变为settled状态。所以一个已settled的promise调用.then(yey,nay)时将当即把一个microtask任务加入microtasks任务队列。这就是为何 promise1 和 promise2 在 script end 以后打印,由于正在运行的代码必须在处理 microtasks 以前完成。promise1 和 promise2 在 setTimeout 以前打印,由于 microtasks 老是在下一个 task 以前执行。
好,一步一步的运行:
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });
没错,就是上面这个,我作了一个 step-by-step 动画图解。你周六是怎么过的?和朋友们一块儿出去玩?我没有出去。嗯,若是搞不明白个人使人惊叹的UI设计界面,点击上面的箭头试试。
浏览器实现差别
一些浏览器的打印结果:
script start script end setTimeout promise1 promise2
在 setTimeout 以后运行 promise 的回调,就好像将 promise 的回调看成一个新的 task 而不是 microtask。
这多少情有可原,由于 promise 来自 ECMAScript 规范而不是 HTML 规范。ECAMScript 有一个概念 job,和 microtask 类似,可是二者的关系在邮件列表讨论中没有明确。不过,通常共识是 promise 应该是 microtask 队列的一部分,而且有充足的理由。
将 promise看成task(macrotask)会带来一些性能问题,由于回调没有必要由于task相关的事(好比渲染)而延迟执行。与其它 task 来源交互时它也产生不肯定性,也会打断与其它 API 的交互,不事后面再细说。
我提交了一条 Edge 反馈,它错误地将 promises 看成 task。WebKit nightly 作对了,因此我认为 Safari 最终会修复,而 Firefox 43 彷佛已经修复。
有趣的是 Safari 和 Firefox 发生了退化,而以前的版本是对的。我在想这是否只是巧合。
动手试一试是一种办法,查看相对于promise和setTimeout如何打印,尽管这取决于实现是否正确。
一种方法是查看规范:
将一个 task 加入队列: step 14 of setTimeout
将 microtask 加入队列:step 5 of queuing a mutation record
如上所述,ECMAScript 将 microtask 称为 job:
调用 EnqueueJob 将一个 microtask 加入队列:step 8.a of PerformPromiseThen
如今,让咱们看一个更复杂的例子。一个有心的学徒 :“可是他们尚未准备好”。别管他,你已经准备好了,让咱们开始……
在发出这篇文章以前,我犯过一个错误。下面是一段html代码:
<div class="outer"> <div class="inner"></div> </div>
给出下面的JS代码,若是click 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 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);
继续,在查看答案以前先试一试。 线索:logs可能会发生屡次。
点击inner区域触发click事件:
click div.inner :
click promise mutate click promise mutate timeout timeout
click div.outer :
click promise mutate timeout
和你猜测的有不一样吗?若是是,你获得的结果可能也是正确的。不幸的是,浏览器实现并不统一,下面是各个浏览器下测试结果:
触发 click 事件是一个 task,Mutation observer 和 promise 的回调 加入microtask列队,setTimeout 回调加入task列队。所以运行过程以下:
点击内部区域触发内部区域点击事件 -> 冒泡到外部区域 -> 触发外部区域点击事件 这里要注意一点: setTimeout 执行时机在冒泡以后,由于也是在microtask以后,准确的说是在最后的时机执行了。
堆栈为空以后将会执行microtasks里面的任务。
因为冒泡, click函数再一次执行。
最后将执行setTimeout。
因此 Chrome 是对的。对我来讲新发现是,microtasks 在回调以后运行(只要没有其它的 Javascript 在运行),我原觉得它只能在一个task 的末尾执行。这个规则来自 HTML 规范,调用一个回调:
If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: Cleaning up after a callback step 3
一个 microtask checkpoint 逐个检查 microtask队列,除非咱们已经在处理一个 microtask 队列。相似地,ECMAScript 规范这么说 jobs:
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 中"can be"变成了"must be"。
对于 mutation callbacks,Firefox 和 Safari 都正确地在内部区域和外部区域单击事件之间执行完毕,清空了microtask 队列,可是 promises 列队的处理看起来和chrome不同。这多少情有可原,由于 jobs 和 microtasks 的关系不清楚,可是我仍然指望在事件回调之间处理。Firefox ticket. Safari ticket.
对于 Edge,咱们已经看到它错误的将 promises 看成 task,它也没有在单击回调之间清空 microtask 队列,而是在全部单击回调执行完以后清空,因而总共只有一个 mutate 在两个 click 以后打印。 Bug ticket.
仍然使用上面的例子,假如咱们运行下面代码会怎么样:
inner.click();
跟以前同样,它会触发 click 事件,不过是经过代码而不是实际的交互动做。
下面是各个浏览器的运行状况:
我发誓我一直在Chrome 中获得不一样的结果,我已经更新了这个表许许屡次了。我以为我是错误地测试了Canary。假如你在 Chrome 中获得了不一样的结果,请在评论中告诉我是哪一个版本。
这里介绍了它是怎样发生的:
将Run srcipt加入Tasks队列,将inner.click加入执行堆栈:
执行click函数:
按顺序执行,分别将setTimeout加入Tasks队列,将Promise MultationObserver加入microtasks队列:
click函数执行完毕以后,咱们没有去处理microtasks队列的任务,由于此时堆栈不为空:
咱们不能将 MultationObserver加入microtasks队列,由于有一个等待处理的 MultationObserver:
如今堆栈为空了,咱们能够处理microtasks队列的任务了:
最终结果:
经过对比事件触发,咱们要注意两个地方:JS stack是不是空的决定了microtasks队列里任务的执行;microtasks队列里不能同时有多个MultationObserver。
正确的顺序是:click, click, promise, mutate, promise, timeout, timeout,彷佛 Chrome 是对的。
在每一个listerner callback被调用以后:
If the stack of script settings objects is now empty,perform a microtask checkpoint. — HTML: 回调以后的清理第三步
以前,这意味着 microtasks 在事件回调之间运行,可是如今.click()让事件同步触发,所以调用.click()的脚本仍处于回调之间的堆栈中。上面的规则确保了 microtasks 不会中断正在执行的JS代码。这意味着 microtasks 队列不会在事件回调之间处理,而是在它们以后处理。
重要,它会在偏角处咬你(疼)。我就遇到了这个问题,我在尝试为IndexedDB建立一个使用promises而不是奇怪的IDBRequest对象的简单包装库时遇到了此问题。它让 IDB 用起来颇有趣。
当 IDB 触发成功事件时,相关的 transaction 对象在事件以后转为非激活状态(第四步)。若是我建立的 promise 在这个事件发生时被resolved,回调应当在第四步以前执行,这时这个对象仍然是激活状态。可是在 Chrome 以外的浏览器中不是这样,致使这个库有些无用。
实际上你能够在 Firefox 中解决这个问题,由于 promise polyfills 如 es6-promise 使用 mutation observers 执行回调,它正确地使用了 microtasks。而它在 Safari 下彷佛存在竞态条件,不过这多是由于他们糟糕的 IDB 实现。不幸的是 IE/Edge 不一致,由于 mutation 事件不在回调以后处理。
但愿不久咱们能看到一些互通性。
总结:
microtasks 按序执行,在下面状况时执行:
但愿你如今明白了事件循环,或者至少获得一个借口出去走一走,躺一躺。
呃,还有人在吗?Hello?Hello?
感谢 Anne van Kesteren, Domenic Denicola, Brian Kardell 和 Matt Gaunt 校对和修正。是的,Matt 最后仍是看了此文,我没必要把他整成发条橙了。
1.microtask队列就会被处理的时机
(1)只要没有其余JavaScript代码在执行中, (2)而且在每一个task队列的任务结束时, microtask队列就会被处理。
也就是说能够在执行一个task以后连续执行多个microtask。
2. promise相关
(1)promise一旦建立就会立刻执行 (2)当状态变为settled的时候,callback才会被加入microtask 队列
因此要注意promise建立和callback被执行的时机。