背景&目的

在前端面试过程中经常会问到关于代码执行顺序的问题,通常考察的是对浏览器的事件循环、宏任务及微任务的理解深度。掌握这些,对于日常写代码、面试都有帮助。

正文

首先,我们先来看一道题 题1:

(大家可以边看边在心里想出自己觉得正确的答案,下面每道题都会带有gif格式图片的题目解析,帮助大家理解~ ps:
图中数字代表代码行数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1 console.log(1);
2 setTimeout(() => {
3 console.log(2);
4 Promise.resolve().then(() => {
5 console.log(3);
6 });
7 }, 0);
8 new Promise((resolve, reject) => {
9 console.log(4);
10 resolve(5);
11 }).then((data) => {
12 console.log(data);
13 });
14 setTimeout(() => {
15 console.log(6);
16 }, 0);

答案是:1 4 5 2 3 6

这道题其实考察的是对浏览器中事件循环的了解程度。众所周知,浏览器中JS代码单线程执行,所有的同步代码在主线程中执行,形成执行栈。那浏览器中的异步机制是如何实现的呢?
首先,异步任务会被依次放入异步任务队列中,当主线程中的同步任务完成以后,浏览器会轮询去异步任务队列中取出异步任务来执行。

JS代码中的异步任务可进一步分为宏任务(macrotask)与微任务(microtask)。
宏任务包括:script代码、setTimeout、setInterval、I/O、UI render
微任务包括:promise.then、Object.observe(已废弃)、MutationObserver

宏任务和微任务会被加入各自的队列中。
当主线程执行完毕后,浏览器会先去清空微任务队列,依次取出微任务队列中的微任务执行,执行过程中如果产生新的微任务,则追加到当前微任务队列的末尾等待执行。
当微任务队列清空后,浏览器会从宏任务队列中取出一个宏任务执行。宏任务执行完毕再去清空微任务队列,微任务队列清空后再取出一个宏任务来执行。如此反复,直至宏任务队列和微任务队列全部清空。
浏览器事件循环流程图
需要注意的是,宏任务和微任务队列中新产生的微任务都会追加到当前微任务队列队尾等待执行,而不是追加到下一个循环的微任务队列中。因此如果微任务队列的清空过程中持续产生新的微任务,会造成微任务卡死。
此时再回看题1,可得出如下结论:

题1代码执行顺序
输出与答案一致。

题2:

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

答案:2 4 3 1

在分析当前代码时需要注意的是,Promise对象的resolve部分的代码是当前主线程/宏任务的一部分,并不是微任务,Promise对象的then和catch代码段才是微任务。因此最先输出的是2和4,然后才是微任务队列中的3,最后是宏任务中的1。

题2代码执行顺序

题3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1 async function async1() {
2 console.log(1);
3 const result = await async2();
4 console.log(3);
5 }
6 async function async2() {
7 console.log(2);
8 }
9 Promise.resolve().then(() => {
10 console.log(4);
11 });
12 setTimeout(() => {
13 console.log(5);
14 });
15 async1();
16 console.log(6);

答案:1 2 6 4 3 5

该题需要注意的是,由于await方法返回的是一个Promise对象,因此await方法执行完毕后续的代码都应该归入微任务队列,因此console.log(3)应该被加入微任务队列等待执行。

题3代码执行顺序

题4:

1
2
3
4
5
1 function sleep(time) {
2 let startTime = new Date()
3 while (new Date() - startTime < time) {}
4 console.log('1s over')
5 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
1 function sleep(time) {
2 let startTime = new Date()
3 while (new Date() - startTime < time) {}
4 console.log('1s over')
5 }
6 setTimeout(() => {
7 console.log('setTimeout - 1')
8 setTimeout(() => {
9 console.log('setTimeout - 1 - 1')
10 sleep(1000)
11 })
12 new Promise(resolve => {
13 console.log('setTimeout - 1 - resolve')
14 resolve()
15 }).then(() => {
16 console.log('setTimeout - 1 - then')
17 new Promise(resolve => resolve()).then(() => {
18 console.log('setTimeout - 1 - then - then')
19 })
20 })
21 sleep(1000)
22 })
23 setTimeout(() => {
24 console.log('setTimeout - 2')
25 setTimeout(() => {
26 console.log('setTimeout - 2 - 1')
27 sleep(1000)
28 })
29 new Promise(resolve => resolve()).then(() => {
30 console.log('setTimeout - 2 - then')
31 new Promise(resolve => resolve()).then(() => {
32 console.log('setTimeout - 2 - then - then')
33 })
34 })
35 sleep(1000)
36 })

浏览器输出为:

1
2
3
4
5
6
7
8
9
10
11
12
13
setTimeout - 1
setTimeout - 1 - resolve
1s over
setTimeout - 1 - then
setTimeout - 1 - then - then
setTimeout - 2
1s over
setTimeout - 2 - then
setTimeout - 2 - then - then
setTimeout - 1 - 1
1s over
setTimeout - 2 - 1
1s over

该题需要注意的是微任务执行过程中产生的新的微任务也是追加在当前微任务队列末尾等待执行。

题4代码执行顺序

总结:

浏览器中的事件循环机制较为简单,如果将主线程也看作一个宏任务的话,那么浏览器的事件循环机制可看作依次执行以下1、2两点:

  • 从宏任务队列中取出一个宏任务执行
  • 清空微任务队列
    如下面的伪代码所示:
    1
    2
    3
    4
    while(true) { 
    宏任务队列.shift();
    微任务队列全部任务;
    }

需要注意的是:

  • 宏任务队列和微任务队列都遵循先进先出原则,先被加入队列的任务优先被取出执行

  • Promise对象的resolve部分不是微任务,then和catch部分才是,即

    1
    2
    3
    4
    5
    6
    new Promise((resolve,reject) => {
    console.log('同步');
    resolve();
    }).then(() => {
    console.log('异步');
    })
  • 微任务执行过程中产生的新的微任务追加到当前微任务队列队尾等待本轮事件循环执行

  • await方法返回的是一个Promise对象,因此await方法执行完毕,后续代码都应归入微任务队列(可结合题4理解)

  • Node环境的事件循环与浏览器不完全一致

希望本文对大家有帮助。