JavaScript 任务的执行顺序

本文最后更新于 2024年4月9日 下午

前言

上一文,我们介绍了Promise对象,Promise结束时执行的是异步操作,但这里提到的异步操作他的执行顺序是怎样的?

让我看一个例子:

1
2
3
4
5
6
7
var p = new Promise(function (resolve, reject) {
console.log("start");
resolve("ok");
});

p.then((msg) => console.log(msg));
console.log("end");

咦~这里也许会十分让人困惑,Promise对象定义后会立即执行,按照代码的顺序,应该是先执行then语句啊。

但是这样想就在是按照以往同步操作的思路去思考,但是then语句中执行的是异步操作,所以执行顺序与我们所想不一致。

同步任务与异步任务

如图所示,程序首先将同步任务加入任务栈,任务栈中的同步任务将首先执行,同步任务执行后从异步任务队列中取出任务加入任务栈,接着执行任务栈中的任务。

对应上文的程序,执行顺序则为:

  1. 执行Promise语句,打印出start
  2. then语句加入异步任务队列。
  3. 打印出end
  4. 同步任务执行完毕,执行异步任务then语句,打印出ok

宏任务与微任务

提出问题

我们再来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log(1)

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

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

console.log(4)

如果先执行同步任务打印出的14,接下来应该执行异步任务,那么是不是应该先打印2,但为什么先打印的是2呢?

两种任务

这里需要引入宏任务和微任务的概念。所以宏任务与微任务并非为异步任务的子集,而是任务形式的另一种划分。

宏任务

宏任务(macrotask),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了能够使得宏任务与DOM任务能够有序的执行,会在一个宏任务执行结束后,在下一个宏任务执行开始前,对页面进行重新渲染

宏任务包括:

# 浏览器 Node.js
主代码块
setTimeout
setInterval
setImmediate x
requestAnimationFrame x

优先级:主代码块 > setImmediate > MessageChannel > setTimeout / setInterval

微任务

微任务(microtask),可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前task任务后,下一个task之前,在渲染之前。

微任务包括:

# 浏览器 Node
process.nextTick x
MutationObserver x
Promise

优先级:process.nextTick > Promise > MutationObserver


在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

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

复习

看一个例子

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
37
38
39
40
//主线程直接执行
console.log('1');
//丢到宏事件队列中
setTimeout(function () {
console.log('2');
process.nextTick(function () {
console.log('3');
})
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5')
})
})
//微事件1
process.nextTick(function () {
console.log('6');
})
//主线程直接执行
new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
//微事件2
console.log('8')
})
//丢到宏事件队列中
setTimeout(function () {
console.log('9');
process.nextTick(function () {
console.log('10');
})
new Promise(function (resolve) {
console.log('11');
resolve();
}).then(function () {
console.log('12')
})
})
  1. 首先执行主代码中的同步任务,首先打印1,然后将setTimeout中的函数加入宏任务队列,nextTick加入微任务队列。
  2. 执行主代码Promise中的代码,打印7then方法加入微任务队列,第二个setTimeout加入宏任务队列,主代码执行结束。
  3. 开始执行微任务队列中的任务,打印68
  4. 执行宏任务队列中的下一个任务,打印2,将nextTick加入微任务队列,执行Promise,打印4,并将该对象的then加入微任务队列。
  5. 再次开始执行微任务队列中的任务,打印35
  6. 再开始执行第二个setTimeout中的代码,打印9,微任务队列加入nextTick,执行Promise打印11then加入微任务队列。
  7. 执行微任务队列,打印1012

这里需要注意,nextTick的优先级高于then方法,因此如果将nextTick移至then之后,还是会先执行。

再看一个更复杂的例子

须知:

由于因为async await 本身就是promise generator的语法糖。所以await后面的代码是微任务,所以

1
2
3
4
5
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}

等价于

1
2
3
4
5
6
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}

问题:

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
async function async1() {
console.log('async1 start');

await async2();
setTimeout(function() {
console.log('setTimeout1')
},0)

/* 等价于
Promise.resolve(async2()).then(() => {
setTimeout(function () {
console.log('setTimeout1')
}, 0)
})
*/
}
async function async2() {

setTimeout(function() {
console.log('setTimeout2')
},0)
}
console.log('script start');

setTimeout(function() {
console.log('setTimeout3');
}, 0)
async1();

new Promise(function(resolve) {
console.log('promise1');
resolve();
}).then(function() {
console.log('promise2');
});
console.log('script end');
  1. 执行主代码中定义的async1async2,打印script startsetTimeout加入宏任务队列。

  2. 执行async1,打印async1 start,执行await,也就是先执行async2,再在微任务队列中加入then后的方法。

  3. 执行Promise,打印promise1,并在微任务队列中加入then后的内容,打印script end,主代码执行结束。

  4. 执行微任务队列中的内容,队列中的第一个任务是一个setTimeout,也就是在宏任务队列中加入内容,第二个微任务是打印promise 2,微任务执行结束。

  5. 执行下一个宏任务,打印setTimeout3

  6. 没有微任务,执行下一个宏任务,打印setTimeout2

  7. 没有微任务,执行下一个宏任务,打印setTimeout1

这里有更多的异步笔试题


JavaScript 任务的执行顺序
https://siegelion.cn/2021/02/04/JavaScript 任务的执行顺序/
作者
siegelion
发布于
2021年2月4日
许可协议