开始

首先看一段代码:

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

打印顺序是什么?
正确答案是
script start, script end, promise1, promise2, setTimeout
但是在不同浏览器上的结果却是让人懵逼的。

再看一段代码:

  1. async function async1() {
  2. console.log('A');
  3. await async2()
  4. console.log('B');
  5. }
  6. async function async2() {
  7. console.log('C');
  8. }
  9. console.log('D');
  10. setTimeout(function() {
  11. console.log('E');
  12. })
  13. async1()
  14. new Promise(function(resolve) {
  15. console.log('F');
  16. resolve()
  17. }).then(function() {
  18. console.log('G');
  19. })
  20. console.log('H');
  21. // D A C F H B G E

为什么会出现这样打印顺序呢

要理解这些你首先需要对事件循环机制处理宏任务和微任务的方式有了解。
每个线程都会有它自己的event loop(事件循环),所以都能独立运行。然而所有同源窗口会共享一个event loop以同步通信。event loop会一直运行,来执行进入队列的宏任务。一个event loop有多种的宏任务源,这些宏任务源保证了在本任务源内的顺序。但是浏览器每次都会选择一个源中的一个宏任务去执行。这保证了浏览器给与一些宏任务(如用户输入)以更高的优先级。

宏任务(task)

浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染 (task->渲染->task->…)
鼠标点击会触发一个事件回调,需要执行一个宏任务,然后解析HTMl。还有下面这个例子,setTimeout

setTimeout的作用是等待给定的时间后为它的回调产生一个新的宏任务。这就是为什么打‘setTimeout’在‘script end’之后。因为打印‘script end’是第一个宏任务里面的事情,而‘setTimeout’是另一个独立的任务里面打印的。

微任务(Microtasks )

微任务通常来说就是需要在当前 task 执行结束后立即执行的任务,比如对一系列动作做出反馈,或者是需要异步的执行任务而又不需要分配一个新的 task,这样便可以减小一点性能的开销。只要执行栈中没有其他的js代码正在执行且每个宏任务执行完,微任务队列会立即执行。如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行。微任务包括了mutation observe的回调还有接下来的例子promise的回调

一旦一个pormise有了结果,或者早已有了结果(有了结果是指这个promise到了fulfilled或rejected状态),他就会为它的回调产生一个微任务,这就保证了回调异步的执行即使这个promise早已有了结果。所以对一个已经有了结果的promise调用.then(yey, nay)会立即产生一个微任务。这就是为什么‘promise1’,’promise2’会打印在‘script end’之后,因为所有微任务执行的时候,当前执行栈的代码必须已经执行完毕。‘promise1’,’promise2’会打印在‘setTimeout’之前是因为所有微任务总会在下一个宏任务之前全部执行完毕。

js微任务和宏任务 - 图1

js微任务和宏任务 - 图2

如何分辨宏任务和微任务?

实际测试是一种方法,观察日志打印顺序与promise和setTimeout的关系,但是首先浏览器对这两者的实现要正确。
还有一个稳妥方法就是看文档,比如setTimeout是宏任务,mutation是微任务。
ECMAScript 中把微任务叫做jobs,EnqueueJob是微任务。

优先级

(事件队列中的每一个事件都是一个macrotask)
宏任务—优先级:主代码块 > setImmediate > MessageChannel > setTimeout / setInterval
比如:setImmediate指定的回调函数,总是排在setTimeout前面

微任务—优先级:process.nextTick > Promise > MutationObserver

更多例子

  1. setTimeout(first(),0); //或者setTimeout(first());
  2. new Promise(function(resolve){
  3. console.log('2');
  4. resolve();
  5. }).then(function(){
  6. console.log('3')
  7. });
  8. console.log('4');
  9. function first(){
  10. console.log('1')
  11. }

结果是 1 2 4 3,因为在setTimeout里面,调用的是first(),带了(),而不是first

  1. setTimeout(first,0);
  2. new Promise(function(resolve){
  3. console.log('2');
  4. resolve();
  5. }).then(function(){
  6. console.log('3')
  7. });
  8. console.log('4');
  9. function first(){
  10. console.log('1')
  11. }

结果是 2 4 3 1

总结

  • 宏任务按顺序执行,且浏览器在每个宏任务之间渲染页面
  • 所有微任务也按顺序执行,且在以下场景会立即执行所有微任务
    • 每个回调之后且js执行栈中为空。
    • 每个宏任务结束后。

参考

  1. 译文:JS事件循环机制(event loop)之宏任务、微任务
  2. demo—Tasks, microtasks, queues and schedules