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。
