事件循环与异步机制 (Event Loop)
JavaScript 之所以能以单线程处理高并发、无阻塞 I/O,全靠事件循环 (Event Loop) 机制。
1. 为什么需要 Event Loop?
JavaScript 是单线程的(为了避免复杂的 DOM 同步问题)。
- 如果所有任务都是同步的(Synchronous),遇到耗时操作(如网络请求、图片加载),整个页面就会卡死(阻塞)。
- 为了解决这个问题,JS 引入了异步 (Asynchronous) 机制。
- Event Loop 就是协调同步任务和异步任务的"交通指挥官"。
2. 宏任务与微任务
在深入流程之前,先区分两种异步任务队列。
2.1 宏任务 (MacroTask / Task)
宏任务是由宿主环境(浏览器 / Node.js)发起的任务。
script(整体代码)setTimeoutsetIntervalsetImmediate(Node.js 独有)I/O操作 (UI 交互、网络请求完成)requestAnimationFrame(有时归类为渲染任务)
2.2 微任务 (MicroTask)
微任务是由 JS 引擎(V8)自身发起的任务。
Promise.then/.catch/.finallyprocess.nextTick(Node.js 独有,优先级最高)MutationObserver
记忆口诀:微任务是 VIP,每次干完一个宏任务,都要把所有排队的 VIP 微任务处理完,才能进行下一个宏任务。
3. Event Loop 执行流程
浏览器的 Event Loop 遵循以下步骤:
- 执行同步代码:从上到下执行调用栈(Call Stack)中的同步任务。
- 清空调用栈:同步代码执行完毕,调用栈清空。
- 执行微任务队列:取出微任务队列(MicroTask Queue)中的所有任务,依次执行,直到队列为空。(如果在执行微任务过程中产生了新的微任务,也会在这一轮里立刻执行掉)。
- 渲染页面:(如果有必要) 浏览器尝试进行 DOM 渲染更新。
- 执行一个宏任务:从宏任务队列(MacroTask Queue)中取出一个最早的任务入栈执行。
- 回到第 3 步:宏任务执行完后,再次检查微任务队列...
循环模型:
Event Loop Start
|
v
[执行同步代码 (Script)]
|
v
[清空微任务队列 (All Microtasks)] <---+
| |
v |
[渲染 UI (Optional)] |
| |
v |
[取出一个宏任务执行 (One Macrotask)] -+4. 实战代码分析
示例 1:基础顺序
javascript
console.log('1'); // 同步
setTimeout(function() {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(function() {
console.log('3'); // 微任务
});
console.log('4'); // 同步输出:1 -> 4 -> 3 -> 2
解析:
- 执行同步
log('1')。 - 遇到
setTimeout,放入宏任务队列。 - 遇到
Promise.then,放入微任务队列。 - 执行同步
log('4')。 - 同步结束,检查微任务,执行
log('3')。 - 微任务结束,检查宏任务,执行
log('2')。
示例 2:微任务插队
javascript
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(function() {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timer2');
}, 0);输出:timer1 -> promise1 -> timer2
解析:
- 两个
setTimeout进入宏任务队列:[timer1, timer2]。 - 取出
timer1执行,打印timer1。 timer1内部产生了一个微任务promise1,放入微任务队列。timer1宏任务结束。- Event Loop 检查微任务队列,发现有
promise1,执行并打印。 - 微任务清空,开始下一个宏任务
timer2,打印timer2。
示例 3:复杂的嵌套
javascript
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
});
new Promise((resolve, reject) => {
console.log('4'); // Promise 构造函数是同步的!
resolve(5);
}).then((data) => {
console.log(data);
});
setTimeout(() => {
console.log('6');
});
console.log('7');输出:1 -> 4 -> 7 -> 5 -> 2 -> 3 -> 6
解析:
log(1)。- 第一个
setTimeout(log(2)) 进宏队列。 new Promise构造器同步执行,log(4)。resolve(5)触发.then,log(5)进微队列。- 第二个
setTimeout(log(6)) 进宏队列。 log(7)。- 同步结束。
- 清微任务:执行
log(5)。 - 取宏任务1:执行
log(2)。产生新微任务log(3)。 - 宏任务1结束,检查微任务。
- 清微任务:执行
log(3)。 - 取宏任务2:执行
log(6)。
5. async/await 在 Event Loop 中的表现
async/await 只是 Promise 的语法糖。
javascript
async function async1() {
console.log('async1 start');
await async2();
// await 下面的代码相当于放在了 Promise.then 中,属于微任务
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
async1();
console.log('script end');输出:async1 start -> async2 -> script end -> async1 end
解析:
- 执行
async1(),打印async1 start。 - 执行
await async2(),调用async2(),打印async2。 await会阻塞其后的代码,将其放入微任务队列。- 跳出
async1,继续执行同步代码,打印script end。 - 同步结束,执行微任务,打印
async1 end。
6. Node.js 与 浏览器的区别 (简略)
Node.js 的 Event Loop 基于 libuv,早期版本(< v11)在这方面与浏览器有很大不同。 但自 Node 11+ 起,Node.js 尽量向浏览器行为对齐:每执行完一个宏任务,就清空微任务队列。
主要区别在于 process.nextTick:
- 在 Node.js 中,
process.nextTick的优先级高于Promise.then。 - 它会在当前阶段(phase)结束后,下一个阶段开始前立即执行。
7. 总结
- JS 分为同步任务和异步任务。
- 同步任务在 Call Stack 中执行。
- 异步任务分宏任务(DOM, Timer, Network)和微任务(Promise)。
- 执行顺序:同步 -> 所有微任务 -> 渲染 -> 一个宏任务 -> 所有微任务 -> ...
Promise构造函数是同步的,.then是微任务。await会让出线程,其后的代码作为微任务执行。