1. 基本概念
JavaScript 是单线程的语言,但通过事件循环(Event Loop)机制实现了非阻塞的异步执行。事件循环主要包含以下几个关键部分:
- 调用栈(Call Stack)
- 任务队列(Task Queue)/宏任务队列(Macrotask Queue)
- 微任务队列(Microtask Queue)
- Web APIs(浏览器环境)或 C++ APIs(Node.js环境)
2. 执行顺序
JavaScript 代码的执行顺序如下:
- 同步代码(在调用栈中直接执行)
- 微任务(microtask):Promise、process.nextTick、MutationObserver等
- 宏任务(macrotask):setTimeout、setInterval、setImmediate、I/O、UI渲染等
2.1 重要原则:
- 每个宏任务执行完后,都会检查微任务队列
- 如果微任务队列不为空,会先清空微任务队列
- 只有等微任务队列清空后,才会执行下一个宏任务
3. 详细示例
3.1 基础示例
console.log('1'); // 同步代码
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
console.log('4'); // 同步代码
// 输出顺序:1 4 3 2
执行过程解析:
- 同步代码
console.log('1')
直接执行 setTimeout
回调被放入宏任务队列- Promise.then 回调被放入微任务队列
- 同步代码
console.log('4')
直接执行 - 调用栈清空后,执行微任务队列中的任务,输出 ‘3’
- 最后执行宏任务队列中的任务,输出 ‘2’
3.2 复杂示例
console.log('script start'); // 1
setTimeout(() => {
console.log('setTimeout 1'); // 5
Promise.resolve().then(() => {
console.log('promise in setTimeout'); // 6
});
}, 0);
Promise.resolve().then(() => {
console.log('promise 1'); // 3
setTimeout(() => {
console.log('setTimeout 2'); // 7
}, 0);
}).then(() => {
console.log('promise 2'); // 4
});
console.log('script end'); // 2
// 输出顺序:
// script start
// script end
// promise 1
// promise 2
// setTimeout 1
// promise in setTimeout
// setTimeout 2
执行过程详细解析:
-
同步代码执行阶段:
- 执行
console.log('script start')
- 遇到第一个 setTimeout,将其回调函数放入宏任务队列
- 遇到 Promise.resolve().then(),将第一个 then 回调放入微任务队列
- 执行
console.log('script end')
- 执行
-
第一轮微任务执行阶段:
- 执行第一个 then 回调,输出
promise 1
- 遇到第二个 setTimeout,将其回调放入宏任务队列
- 第一个 then 执行完成会链式调用第二个 then,将其放入微任务队列
- 继续执行微任务队列,输出
promise 2
- 执行第一个 then 回调,输出
-
第一个宏任务执行阶段:
- 执行第一个 setTimeout 的回调,输出
setTimeout 1
- 遇到 Promise.resolve().then(),将其回调放入微任务队列
- 该宏任务执行完毕,检查微任务队列
- 执行微任务,输出
promise in setTimeout
- 执行第一个 setTimeout 的回调,输出
-
第二个宏任务执行阶段:
- 执行第二个 setTimeout 的回调,输出
setTimeout 2
- 执行第二个 setTimeout 的回调,输出
关键点说明:
- Promise 链式调用的 then 会依次进入微任务队列
- 每个宏任务执行完后,都会检查并清空微任务队列
- setTimeout 虽然延时为 0,但会在当前同步代码和微任务都执行完后才执行
3.3 async/await 示例
async function async1() {
console.log('async1 start'); // 2
await async2();
console.log('async1 end'); // 6
}
async function async2() {
console.log('async2'); // 3
}
console.log('script start'); // 1
setTimeout(() => {
console.log('setTimeout'); // 8
}, 0);
async1();
new Promise((resolve) => {
console.log('promise1'); // 4
resolve();
}).then(() => {
console.log('promise2'); // 7
});
console.log('script end'); // 5
// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
执行过程详细解析:
-
同步代码执行阶段:
- 执行
console.log('script start')
- 遇到 setTimeout,将其回调放入宏任务队列
- 遇到 async1() 调用:
- 进入 async1 函数,执行
console.log('async1 start')
- 遇到 await async2(),立即执行 async2 函数
- 执行 async2 中的
console.log('async2')
- await 会将后面的代码(
console.log('async1 end')
)放入微任务队列
- 进入 async1 函数,执行
- 执行 new Promise 中的同步代码
console.log('promise1')
- Promise.resolve() 将 then 的回调放入微任务队列
- 执行
console.log('script end')
- 执行
-
微任务执行阶段:
- 执行 await 后面的代码,输出
async1 end
- 执行 Promise.then 的回调,输出
promise2
- 执行 await 后面的代码,输出
-
宏任务执行阶段:
- 执行 setTimeout 的回调,输出
setTimeout
- 执行 setTimeout 的回调,输出
关键点说明:
- async 函数在执行时,遇到 await 会立即执行 await 后面的表达式
- await 后面的代码会被转换成 Promise.then 的回调,放入微任务队列
- async/await 本质上是 Promise 的语法糖,所以它的执行顺序遵循 Promise 的规则
- async 函数内部的同步代码会立即执行,而不会进入任务队列
4. Node.js 事件循环的特点
Node.js 的事件循环比浏览器更复杂,包含六个阶段:
- timers:执行 setTimeout 和 setInterval 的回调
- pending callbacks:执行系统操作的回调
- idle, prepare:仅系统内部使用
- poll:检索新的 I/O 事件,执行 I/O 相关的回调
- check:执行 setImmediate 的回调
- close callbacks:执行 close 事件的回调
// Node.js 环境示例
const fs = require('fs');
console.log('start');
setImmediate(() => {
console.log('setImmediate');
});
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise');
});
process.nextTick(() => {
console.log('nextTick');
});
fs.readFile(__filename, () => {
console.log('readFile');
});
console.log('end');
// 输出顺序:
// start
// end
// nextTick
// promise
// setTimeout
// setImmediate
// readFile
5. 面试题实践
5.1 示例一:setTimeout与Promise的执行顺序
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
解析
同步任务console.log('1')
和console.log('4')
最先执行。
Promise的微任务.then()
优先级高于setTimeout的宏任务,因此3
先于2
输出。
最终输出顺序:1, 4, 3, 2
。
5.2 示例二:async/await与定时器混合场景
async function asyncFunc() {
console.log('A');
await Promise.resolve().then(() => console.log('B'));
console.log('C');
}
setTimeout(() => console.log('D'), 0);
asyncFunc();
Promise.resolve().then(() => console.log('E'));
console.log('F');
解析
同步代码按顺序执行A, F
。
await
后的代码console.log('C')
会被推入微任务队列,与E
同属微任务。
微任务执行顺序按入队顺序:B, E, C
。
宏任务D
最后执行。
最终输出顺序:A, F, B, E, C, D
。
6. 总结
- 执行顺序:同步代码 > 微任务 > 宏任务。
- 微任务(
Promise
、process.nextTick
)先于宏任务(setTimeout
、setInterval
)执行。 await
会暂停函数执行,后续代码变为微任务。- 每个宏任务执行完后,都会检查微任务队列。