引言
大家好!今天晚上我们来深入聊聊JavaScript中一个超级重要的概念——事件循环(Event Loop)。理解了它,你就能真正明白JS代码的执行顺序,再也不会被setTimeout
和Promise
的执行顺序搞晕了!废话不多说,直接开始!
基础篇——JS执行机制
1. JavaScript是单线程的
console.log('第一行代码');
alert('阻塞操作'); // 这行代码会阻塞后续代码执行
console.log('第二行代码');
关键点:
- JS只有一个主线程
- 所有同步任务按顺序执行
- 遇到耗时操作会阻塞后续代码
2. 为什么需要事件循环?
因为浏览器需要:
- 处理用户交互
- 执行JS代码
- 渲染页面
- 处理网络请求
所有这些任务都需要排队执行!
核心概念
1. 调用栈(Call Stack)
function foo() {
console.log('foo');
bar();
}
function bar() {
console.log('bar');
}
foo();
调用栈变化过程:
foo()
入栈console.log('foo')
入栈 → 执行 → 出栈bar()
入栈console.log('bar')
入栈 → 执行 → 出栈bar()
出栈foo()
出栈
2. 任务队列(Task Queue)
JS中有两种主要队列:
-
宏任务队列(MacroTask Queue)
setTimeout
setInterval
- I/O操作
- UI渲染
setImmediate
(Node.js)
-
微任务队列(MicroTask Queue)
Promise.then/catch/finally
MutationObserver
process.nextTick
(Node.js)
3. 事件循环工作流程
- 执行同步代码(调用栈)
- 执行完所有同步代码后,检查微任务队列
- 执行所有微任务
- 如有必要,渲染页面
- 从宏任务队列取出一个任务执行
- 重复上述过程
实战分析
案例1:基础执行顺序
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
输出顺序:
- '1'
- '4'
- '3'
- '2'
案例2:嵌套Promise
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
Promise.resolve().then(() => {
console.log('4');
});
});
console.log('5');
输出顺序:
- '1'
- '5'
- '3'
- '4'
- '2'
案例3:复杂场景
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
setTimeout(() => {
console.log('4');
}, 0);
Promise.resolve().then(() => {
console.log('5');
setTimeout(() => {
console.log('6');
}, 0);
});
console.log('7');
输出顺序:
- '1'
- '7'
- '5'
- '2'
- '3'
- '4'
- '6'
进阶理解
1. 为什么微任务优先级高?
因为微任务通常与当前执行的JS代码直接相关(如Promise回调),需要在下一个宏任务执行前完成。
2. requestAnimationFrame
console.log('1');
requestAnimationFrame(() => {
console.log('2');
});
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
requestAnimationFrame
的执行时机在渲染之前,微任务之后。
3. Node.js与浏览器差异
在Node.js中:
process.nextTick
比微任务优先级更高setImmediate
和setTimeout
的执行顺序有特殊规则
常见误区
1. "setTimeout(fn, 0)会立即执行"
错!它只是把回调放入宏任务队列,等待当前调用栈清空后执行。
2. "Promise比setTimeout快"
不完全正确。Promise回调是微任务,而setTimeout是宏任务,执行时机不同。
3. "所有异步代码都是宏任务"
错!Promise、MutationObserver等是微任务。
总结
记住事件循环的核心规则:
- 同步代码最先执行
- 微任务在同步代码执行完后立即执行
- 宏任务在微任务执行完后执行
- 每次执行一个宏任务后,会再次检查微任务队列
理解了这些,你就能准确预测任何JS代码的执行顺序了!
终极测试
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => {
console.log('3');
});
}, 0);
new Promise((resolve) => {
console.log('4');
resolve();
}).then(() => {
console.log('5');
setTimeout(() => {
console.log('6');
}, 0);
});
console.log('7');
答案:
- '1'
- '4'
- '7'
- '5'
- '2'
- '3'
- '6'
你答对了吗?