JavaScript 任务的执行顺序
本文最后更新于 2024年6月7日 下午
前言
上一文,我们介绍了Promise
对象,Promise
结束时执行的是异步操作,但这里提到的异步操作他的执行顺序是怎样的?
让我看一个例子:
1 |
|
咦~这里也许会十分让人困惑,Promise
对象定义后会立即执行,按照代码的顺序,应该是先执行then
语句啊。
但是这样想就在是按照以往同步操作的思路去思考,但是then
语句中执行的是异步操作,所以执行顺序与我们所想不一致。
同步任务与异步任务
如图所示,程序首先将同步任务加入任务栈,任务栈中的同步任务将首先执行,同步任务执行后从异步任务队列中取出任务加入任务栈,接着执行任务栈中的任务。
对应上文的程序,执行顺序则为:
- 执行
Promise
语句,打印出start
。 - 将
then
语句加入异步任务队列。 - 打印出
end
。 - 同步任务执行完毕,执行异步任务
then
语句,打印出ok
。
宏任务与微任务
提出问题
我们再来看一段代码:
1 |
|
如果先执行同步任务打印出的1
和4
,接下来应该执行异步任务,那么是不是应该先打印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 |
|
- 首先执行主代码中的同步任务,首先打印
1
,然后将setTimeout
中的函数加入宏任务队列,nextTick
加入微任务队列。 - 执行主代码
Promise
中的代码,打印7
,then
方法加入微任务队列,第二个setTimeout
加入宏任务队列,主代码执行结束。 - 开始执行微任务队列中的任务,打印
6
与8
。 - 执行宏任务队列中的下一个任务,打印
2
,将nextTick
加入微任务队列,执行Promise
,打印4
,并将该对象的then
加入微任务队列。 - 再次开始执行微任务队列中的任务,打印
3
和5
。 - 再开始执行第二个
setTimeout
中的代码,打印9
,微任务队列加入nextTick
,执行Promise
打印11
,then
加入微任务队列。 - 执行微任务队列,打印
10
与12
。
这里需要注意,
nextTick
的优先级高于then
方法,因此如果将nextTick
移至then
之后,还是会先执行。
再看一个更复杂的例子
须知:
由于因为async await
本身就是promise generator
的语法糖。所以await
后面的代码是微任务,所以
1 |
|
等价于
1 |
|
问题:
1 |
|
-
执行主代码中定义的
async1
与async2
,打印script start
,setTimeout
加入宏任务队列。 -
执行
async1
,打印async1 start
,执行await
,也就是先执行async2
,再在微任务队列中加入then
后的方法。 -
执行
Promise
,打印promise1
,并在微任务队列中加入then
后的内容,打印script end
,主代码执行结束。 -
执行微任务队列中的内容,队列中的第一个任务是一个
setTimeout
,也就是在宏任务队列中加入内容,第二个微任务是打印promise 2
,微任务执行结束。 -
执行下一个宏任务,打印
setTimeout3
。 -
没有微任务,执行下一个宏任务,打印
setTimeout2
。 -
没有微任务,执行下一个宏任务,打印
setTimeout1
。