Tasks, microtasks, queues and schedules

原文地址(英):https://jakearchibald.com/201...

当我告诉Matt Gaunt(作者的同事),我正在谋划写一篇关于在浏览器事件循环(event loop)体系中微任务( microtask )的队列和执行的文章时,他说:“实话告诉你Jake,我对这篇文章是不会感兴趣的”。好吧,不管怎样,既然我已经写了那就让我们坐下来好好享受它,好吗?

事实上,如果视频更符合你的胃口,那么Philips Roberts 在JSConf上关于event loop的演讲会是很好的参考(该演讲不涉及微任务(microtask),但是对事件循环的其他部分都讲得非常好),闲话少说,开始我们的内容。

以下是一小段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');

想想看控制台会按照什么样的顺序打印结果呢?

正确的答案是: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得到的结果始终是正确的。

为什么会是这样

为了搞清楚缘由,你需要明白事件循环(event loop)是如何处理任务(tasks)和微任务(microtasks)的.当这些名词第一次出现的时候,你可能会感到头疼,没关系,深呼吸...

每一个线程都拥有属于自己的事件循环(event loop),也就意味着每一个web worker都会存在自有的事件循环,这样它就能独立运作互不干扰。然而对于同源窗口而言他们共享一个事件循环(event loop),这样他们就可以互相通信了(译者注:根据HTML5.2规范,事件循环分两种,一种是浏览器上下文的,一种是web worker的)。事件循环(event loop)循环执行着进入队列的任务。一个事件循环存在多个任务源,这确保了任务源的执行顺序(译者注:同一个任务源的任务将被添加到相同任务队列,不同任务源的任务可能被添加到不同任务队列),但是在每一次的循环中,浏览器会自主选择哪个源的任务优先执行,这确保了一些性能敏感的任务的优先级,比如用户输入。

任务(tasks,译者注:也叫macro-task)是被调度的,这样浏览器就能很好的将这些任务注入到JavaScript或者Dom中,并且确保这些操作有序执行。在任务之间,浏览器可能会更新视图,从鼠标点击到事件回调,再到html的渲染,以及上面例子提到的setTimeout这些都需要任务调度。

setTimeout等待了给定的延迟时间之后就会为它的回调创建一个新的任务。这就是为什么setTimeout打印在script end之后,因为script end是第一个任务的一部分,而setTimeout对应的是另一个任务,至此,我们快要搞清楚了,我需要你们有足够的耐心看完下一个部分

微任务(Microtasks)通常被安排在当前执行脚本之后,比如对一些批量操作或者一些不会产生新的任务队列的异步处理的反应。每次事件循环中,如果没有其他JavaScript运行并且任务(task)都执行完毕了,那么微任务就会在回调之后被执行。在微任务中排队的任何其他微任务将被添加到队列的末尾并进行处理。微任务包括 MutationObserverPromise的回调(译者注:微任务包括:process.nextTick(Nodejs), Promises, Object.observe, MutationObserver;任务(tasks)包括:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering)。

一旦promise执行或者已经执行就会在队列中插入这个微任务的回调。这确保了promise回调是异步的,即使是这个promise已经执行。因此调用.then(yey,nay)是不会立即执行的,这就是为什么promise1promise2会在script end后打印出来,因为只有在当前脚本执行完成之后微任务才会被执行。也因为微任务通常在下一个任务(tasks)执行之前被执行,所以promise1promise2会在setTimeout之前打印。

让我们一步一步分析,(译者注:跳转到原文step by step示例,这对理解本文非常有用

为什么在一些浏览器上的结果会有不同呢?

一些浏览器打印出来的结果是:script startscript endsetTimeoutpromise1promise2。这些浏览器在promise回调之前调用了setTimeout。这很可能是浏览器把promise回调当做是新任务的一部分而不是微任务。

这种错误某种程度上是可以被原谅的,因为promises规范来源于ECMAScript而不是HTML。ECMAScript定义了类似微任务的“jobs”概念,但是除了一些模糊的邮件讨论之外,这种关系(jobs和microtasks)并不明确。但promises应该作为微任务的一部分这是普遍的共识。

把promise当做是任务将会导致一些性能问题,回调可能没有必要因为某些相关任务(比如渲染)而被延迟。由于与其他任务源的交互这也会导致一些不确定性,并且会中断与其他Api的交互。

把promise归类为微任务已经是很急迫的事情了。Webkit(Safari内核)一直都在做正确的事情,我想Safari最终会解决这和问题,事实上,Firefox43已经修复了这个问题。

真正有趣的是,Safari和Firefox在这里都经历了一次回归,从那以后问题就被修复了。我想知道这是不是一个巧合。

未完待译。

上一篇:leetcode 5736 周赛 单线程cpu 优先队列和排序


下一篇:eclipse中的Tasks(任务)标记