原文:Tasks, microtasks, queues and schedules

任务、微任务,队列和调度

先看下面这道题?思考下 打印顺序?

  1. console.log('script start');
  2. setTimeout(function () {
  3. console.log('setTimeout');
  4. }, 0);
  5. Promise.resolve()
  6. .then(function () {
  7. console.log('promise1');
  8. })
  9. .then(function () {
  10. console.log('promise2');
  11. });
  12. console.log('script end');

正确的答案是:script startpromise1promise2script endsetTimeout。但是由于浏览器的支持,会有一些区别。
Microsoft Edge, Firefox 40, iOS Safari and desktop Safari 8.0.8打印setTimeoutpromise1promise2之前,尽管这看起来有一些混乱。但在FireFox 39和Safari 8.0.7总是正确的。

为何会如此?

要理解这些,你需要知道event loop是如何处理task和microtasks的。

每一个‘线程’, 都有它自己的 event loop,每一个web worker也有它自己的,他可以独立的执行,然而同一个origin下的所有窗口会分享同一个event loop这样他们可以同步通信。event loop会持续的运行,会一直执行队列里的任务。一个event loop 有多个任务源并且在资源内部保证执行顺序(例如IndexedDB定义自己的),但是浏览器会选择哪一个资源去执行一个任务在每一轮的循环中。这会导致浏览器会优先安排对性能要求高的任务例如用户输入。

Tasks是预先安排好的,所以浏览器能从他们的内部进入到JavaScript/DOM领域,并且确保这些动作按照顺序发生。在任务中间,浏览器会渲染更新。从用户的鼠标点击到到一个事件回调需要安排一个任务,紧接着解析HTML。

setTimeout会等待给定的延迟时间后为回调函数安排一个新的任务。这也是为什么setTimeout会在script end之后打印,这是因为script end是第一个任务的一部分,而setTimeout是在单独的任务里打印。

MicroTasks通常是被直接在当前执行脚本的后面,紧接着执行, 例如对批量动作的反应,或者异步执行一些动作而不需要花费一个完整的新任务。微任务队列执行是在回调之后,只要没有其他的JS脚本在执行,会在每一个任务的完成后。在微任务执行期间,一些额外的微任务队列也会被添加到微任务队列的尾部并被执行。微任务包含observer的变更回调,promise回调等。

一旦promise被设置,或者它已经被安排,他就会为他的回调函数安排一个微任务队列。这确保了promise的回调是异步的即使这个promise早就被安排。调用.then(yey, nay)在一个已完成的promise之后会立即添加一个微任务到队列中。这也是为什么promise1promsie2script end之后打印,微任务处理必须在当前的执行脚本完成之后。promise1promise2setTimeout之前打印,微任务在下一个任务之前发生。

一些浏览器有什么不同?

一些浏览器会打印script start, script end, setTimeout, promise1, promise2。他们在执行setTimeout之后执行promise1promise2。可能的原因是他们运行promsie的回调作为一个新的任务而不是微任务。

这是有一定道理的,promise是来自于ECMAScript而不是HTML。ECMAScript有‘jobs’的概念,和微任务类似,但是他们之间的联系并不是很清晰。然而,出于共识,promise应该是微任务队列的一部分。

promsie作为宏任务会导致性能问题,这是由于一些和任务相关的回调函数没必要做延迟,例如渲染。这会引起不能决定论由于和其他任务源的交互,这会打断和其他API的交互。

怎么知道是微任务还是宏任务?