8 个示例理清宏任务微任务执行顺序

179 阅读5分钟

8 个示例理清宏任务微任务执行顺序

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

事件循环相信很多人都了解过,关于宏任务和微任务的介绍和执行顺序也有非常多的介绍,我一开始看了事件循环也觉得不就是这样吗?跟着视频里面的老师做了几道题目就觉得自己已经会了,其实不然,下次遇到另一种变体或者新情况就不会了,比如 promise 中嵌套 setTimeout 顺序会怎么样?setTimeout 中嵌套 promise 会怎么样?两者循环多套几层又会怎样?如果是 setInterval 又会怎样?

话不多说,先上宏任务和微任务的执行顺序的图

宏任务微任务3.jpg

下面就做几道例题理清执行顺序

promise 中嵌套 setTimeout

Promise.resolve()
  .then(function () {
    setTimeout(() => {
      console.log("1");
    }, 0);
  })
  .then(function () {
    console.log("2");
  });

// 答案
// 2,1

2.png

顺序:外层微任务 => 内层微任务 => 内层宏任务

外层微任务,指的是 Promise.resolve().then(),内层微任务 Promise.resolve().then().then(),内存宏任务就是 Promise.resolve().then() 中的 setTimeout(),下文按顺序用 A,B,C 表示

为什么会是 2,1?

当 A 执行完后,微任务队列中又添加了 C,所以会继续执行微任务队列中的 C,而不是 B,当微任务队列没有之后才会执行 B

setTimeout 中嵌套 promise

setTimeout(() => {
  console.log(1);
  Promise.resolve().then(() => {
    console.log(2);
  });
}, 0);

// 答案
// 1,2

3.png

这题很好理解,因为 Promise 是异步的,所以是先输出 1 再输出 2,从宏/微任务的角度,执行完 setTimeout 才会把 Promise.resolve().then() 加入微任务队列,所以它不会抢先执行

注意陷阱!

setTimeout(() => {
  Promise.resolve().then(() => {
    console.log(2);
  });
  console.log(1);
}, 0);

// 答案
// 1,2

如果把 console.log(1) 放到 Promise.resolve().then() 下面还会是 1,2 吗?正如上面所说,Promise 是异步的,Promise 不会堵塞代码,所以 console.log(1) 仍然会输出

进阶套娃

Promise.resolve().then(() => {
  console.log(1);
  setTimeout(() => {
    console.log(2);
  }, 0);
});
setTimeout(() => {
  console.log(3);
  Promise.resolve().then(() => {
    console.log(4);
  });
}, 0);
console.log(5);

// 答案
// 5,1,3,4,2

4.png

顺序:外层微任务 => 内层微任务 => 外层宏任务 => 内层宏任务

这个是相当经典的例题,它的顺序包含了第一个示例中的顺序,而这个示例的顺序也是宏/微任务在事件循环中的调度顺序,这个调度和操作系统中的优先级顺序有点像,如果用操作系统讲解这条题会是怎么样?

5 最先到达,所以 cpu 先执行 5,然后 1,3 同时达到,但是 1 优先级比 3 大,所以 cpu 执行 1,执行完 1 后 2 到达,但是优先级和 3 相同,同等优先级采取先到先执行(FCFS)策略,所以执行 3,然后 3 执行完后,4 到达,此时 cpu 有 2,4 两道进程,4 的优先级比 2 要高,所以执行 2,最后执行 4

所以如果理解足够深刻,完全可以再度精简顺序,由外层微任务 => 内层微任务 => 外层宏任务 => 内层宏任务变成先微后宏

注:面试时不要说先微后宏,我说了以后面试官就在开始在笔记本上打字

我建议按照 从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理 中的运行机制介绍,可以说很久又有很多扩展点

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
  • 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)

再来一题

console.log(1);

setTimeout(function () {
  console.log(2);
}, 0);

Promise.resolve()
  .then(function () {
    console.log(3);
  })
  .then(function () {
    console.log(4);
  });

console.log(5);

// 答案
// 1,4,3,4,2

顺序:外层微任务 => 内层微任务 => 外层宏任务 => 内层宏任务

这题不再解释,都是一样的,可以拿来练练手

setInterval 的情况

setInterval 其实也是宏任务,其顺序依然是 微任务 -> 宏任务

setInterval(function () {
  console.log(1);
}, 1000);

Promise.resolve().then(function () {
  console.log(2);
});

console.log(3);

// 答案
// 3,2,1,...,1

既然都说到了,setInterval 就有必要吧宏任务和微任务一些叽哩旮旯 API 拉出来遛遛了

  1. 宏任务,requestAnimationFrame,setImmediate
  2. 微任务,process.nextTick,Promise.prototype.catch,Promise.prototype.finally

注意,process.nextTick 的优先级要比 Promise.prototype.then 要高

Promise.resolve().then(function () {
  console.log(1);
});
process.nextTick(() => {
  console.log(2);
});

// 答案
// 2,1

5.png

对于 requestAnimationFrame 涉及到了 EventLoop 和浏览器渲染、帧动画、空闲回调的关系,已经超出了文章的核心主线,具体可以去看一下这篇文章,深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系 - ssh-晨曦时梦见兮

总结

宏任务和微任务的执行顺序本身并不难判断,但如果是两者不断嵌套不仔细看的话可能就会掉入陷阱

补充:主代码(同步代码)也属于宏任务,但是为了避免示例中宏任务和微任务的理解出现偏差,没有直接说明,而 setTimeout 也就是实例中所说的宏任务,更具体点应该是异步宏任务

补充练习

我曾经见过一条非常绕的题目,感兴趣的可以做一下,内附超详细讲解图

setTimeout(() => {
  console.log(1);
}, 0);
var p1 = new Promise((res, rej) => {
  res(2);
})
  .then((res) => {
    console.log(res);
    new Promise((res, rej) => {
      res(3);
    })
      .then((v) => {
        console.log(v);
        return 4;
      })
      .then((v) => {
        console.log(v);
      });
    return 5;
  })
  .then((v) => {
    console.log(v);
  });

var p2 = new Promise((res, rej) => {
  res(6);
})
  .then((res) => {
    console.log(res);
    return 7;
  })
  .then((v) => {
    console.log(v);
  });

// 答案
// 2,6,3,5,7,4,1

宏任务和微任务.jpg