原文地址(英): https://jakearchibald.com/201...
当我告诉Matt Gaunt(做者的同事),我正在谋划写一篇关于在浏览器事件循环(event loop)体系中微任务( microtask )的队列和执行的文章时,他说:“实话告诉你Jake,我对这篇文章是不会感兴趣的”。好吧,无论怎样,既然我已经写了那就让咱们坐下来好好享受它,好吗?html
事实上,若是视频更符合你的胃口,那么Philips Roberts 在JSConf上关于event loop的演讲会是很好的参考(该演讲不涉及微任务(microtask),可是对事件循环的其余部分都讲得很是好),闲话少说,开始咱们的内容。html5
如下是一小段JavaScript: 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');
想一想看控制台会按照什么样的顺序打印结果呢?git
正确的答案是:script start
, script end
, promise1
, promise2
, setTimeout
,可是在不一样的浏览器上结果可能会有所不一样。github
Microsoft Edge, Firefox 40, iOS Safari和桌面版Safari 8.0.8会在promise1
,promise2
以前打印出setTimeout
,虽然这多是浏览器厂商间各自竞争的结果,但这未免有些奇怪,由于Firefox 39和Safari 8.0.7获得的结果始终是正确的。web
为了搞清楚原因,你须要明白事件循环(event loop)是如何处理任务(tasks)和微任务(microtasks)的.当这些名词第一次出现的时候,你可能会感到头疼,不要紧,深呼吸...api
每个"线程"都拥有属于本身的事件循环(event loop),也就意味着每个web worker都会存在自有的事件循环并独立运行互不干扰。然而全部同源窗口之间共享一个事件循环(event loop),这样它们就能够同步通讯了(译者注:根据HTML5.2规范,事件循环分两种,一种是浏览器上下文的,一种是web worker的)。事件循环(event loop)老是不断的运行,执行队列中的任务(task)。一个事件循环存在多个任务源,这确保了任务在特定任务源的执行顺序(译者注:同一个任务源的任务将被添加到相同任务队列,不一样任务源的任务可能被添加到不一样任务队列),可是在每一次的循环中,浏览器会自主选择哪一个源的任务优先执行,这确保了一些性能敏感的任务的优先级,好比用户输入。promise
任务(tasks,译者注:也叫macro-task)被放到任务源中,浏览器内部执行转移到JavaScript/DOM领域,而且确保这些 tasks按序执行。在tasks执行期间,浏览器可能更新渲染。来自鼠标点击的事件回调须要安排一个task,解析HTML和setTimeout一样须要。浏览器
setTimeout等待了给定的延迟时间以后就会为它的回调建立一个新的任务。这就是为何setTimeout在script end
以后打印script start
,由于script end
是归属于第一个任务,而setTimeout
对应的是另外一个任务,至此,咱们快要搞清楚了,我须要大家有足够的耐心看完下一个部分app
微任务(Microtasks)队列一般用于存放一些任务,这些任务应该在正在执行的脚本以后当即执行,好比对一批动做做出反应,或者操做异步执行避免建立整个新任务形成的性能浪费。每次事件循环中,若是没有其余JavaScript运行而且任务(task)都执行完毕了,那么微任务就会在回调以后被执行。在微任务中排队的任何其余微任务将被添加到队列的末尾并进行处理。微任务包括 MutationObserver
、Promise
的回调(译者注:微任务包括:process.nextTick(Nodejs), Promises, Object.observe, MutationObserver;任务(tasks)包括:script(总体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering)。webapp
一个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 以前执行。
让咱们一步一步分析,(译者注:跳转到原文step by step示例,这对理解本文很是有用)
一些浏览器打印出来的结果是:script start
,script end
,setTimeout
,promise1
,promise2
。这些浏览器在promise回调以前调用了setTimeout。这极可能是浏览器把promise回调当作是新任务(task )的一部分而不是微任务(microtask)。
这种错误某种程度上是能够被原谅的,由于promises规范来源于ECMAScript而不是HTML。ECMAScript定义了相似微任务的“jobs”概念,可是除了一些模糊的邮件讨论以外,这种关系(jobs和microtasks)并不明确。但promises应该做为微任务的一部分这是广泛的共识。
把promise当作是任务将会致使一些性能问题,回调可能没有必要由于某些相关任务(好比渲染)而被延迟。因为与其余任务源的交互这也会致使一些不肯定性,而且会中断与其余Api的交互。
把promise归类为微任务已是很急迫的事情了。Webkit(Safari内核)一直都在作正确的事情,我想Safari最终会解决这和问题,事实上,Firefox43已经修复了这个问题。
真正有趣的是,Safari和Firefox在这里都经历了一次回归,从那之后问题就被修复了。我想知道这是否是一个巧合。