Promise 和 setTimeout 执行顺序

promise、setTimeout 等异步 API 的执行顺序和区别。

promise 是 ES6 中用于异步编程的一种解决方案。它可以避免回调地狱的问题。setTimeout 是 JavaScript 中的定时函数,可以在一定的时间后执行代码。当 setTimeout 和 promise 同时使用时,它们两个的执行顺序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
let promise = new Promise((resolve, reject) => {
console.log('Promise') // 主进程
resolve('Promise then')
})
setTimeout(() => {
console.log('timeout') // 宏任务
}, 1000)

promise.then(res => { // 微任务
console.log(res)
})

console.log('start') // 主进程
  1. promise 对象创建时处于 pending 状态。new Promise 里面的内容会立即执行
  2. setTimeout 会首先执行,因为它不需要等待任何操作,作为一个异步任务直接进入事件循环队列。
  3. promise 的 then/catch 被定义,但不会立即执行。
  4. 主线程继续执行同步代码,将 then/catch 放入微任务队列。
  5. 当主线程执行栈为空时,从微任务队列取出 then/catch 回调并执行。
  6. 同时 setTimeout 中的回调函数,会在计时结束后被放入宏任务队列。
  7. 主线程执行完所有微任务后,会检查宏任务队列,取出 setTimeout 回调函数并执行。

当 JS 引擎执行异步代码时,会按照这个顺序来执行:
主线程代码 > 微任务队列 > 宏任务队列
也就是:

  1. 首先执行主线程上的同步代码
  2. 主线程代码执行完毕后,检查微任务队列,依次执行所有微任务队列中的任务
  3. 微任务队列执行完毕后,检查宏任务队列,依次执行所有宏任务队列中的任务
  4. 然后循环往复上述过程
    其中:
  • 主线程代码指的是普通的同步代码
  • 微任务包括 promise.then/catch、MutationObserver 等
  • 宏任务包括 setTimeout、setInterval、DOM 事件回调、I/O 操作的回调(网络请求、文件读写、数据库操作)、postMessage/MessageChannel 的事件等
    所以在异步代码中,我们往往先定义微任务,然后再定义宏任务。这样可以利用微任务的执行优先级,使得需要先执行的逻辑放在微任务中,从而控制代码的执行顺序。

执行结果答案

1
2
3
4
5
执行结果
Promise
start
Promise then
timeout

习题,如果我们在 Promise 里面有 setTimeout,外面也有 setTimeout,那么谁会先执行,最终结果是什么呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setTimeout(() => {
console.log('timeout') // 宏任务
}, 1000)
let promise = new Promise((resolve, reject) => {
console.log('Promise') // 主进程
setTimeout(() => {
resolve('Promise then') // 宏任务
}, 1000)
})
promise.then(res => { // 微任务
console.log(res)
})

console.log('start') // 主进程