JavaScript事件循环:从火锅店经营到并发大师
痛点暴击 🥘
"说说JS的事件循环机制?"
当面试官抛出这个问题时,你的大脑是否像火锅店高峰期:
- 订单(任务)不断涌入
- 服务员(主线程)手忙脚乱
- 后厨(Web API)压力山大
- 传菜员(回调队列)来回奔波
最后呈现给客人的却是:未煮熟的毛肚(未处理的任务)和煮过头的脑花(阻塞的UI)...
跨界破冰:事件循环就像火锅店 🍲
场景映射:
class 火锅店 {
constructor() {
this.大堂 = []; // 调用栈
this.后厨 = new Map(); // Web APIs
this.传菜区 = []; // 任务队列
}
接待(顾客) { // 主线程
this.处理订单(顾客);
this.检查后厨();
this.上菜循环();
}
}
核心员工职责:
- 大堂经理(调用栈):现场处理简单订单
- 后厨团队(Web API):处理耗时操作(setTimeout等)
- 传菜小哥(事件循环):不断检查后厨完成情况
毒性技术拆解 🔥
1. 经典面试题翻车现场
console.log('锅底'); // 同步任务
setTimeout(() => {
console.log('肥牛'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('毛肚'); // 微任务
});
console.log('蘸料');
// 输出顺序:锅底 → 蘸料 → 毛肚 → 肥牛
2. 九大诡异现象(附解法)
1. setTimeout(fn, 0) 真的0秒执行?
- 真相:至少4ms的浏览器限制
2. 点击事件比setTimeout先触发?
- 原理:事件冒泡属于宏任务
3. 为什么Promise.then比setTimeout快?
- 机制:微任务优先级更高
4. requestAnimationFrame在渲染前还是后执行?
- 冷知识:就像火锅加汤,总是在渲染前执行
5. 为什么postMessage有时比setTimeout快?
- 秘密:跨文档通信有VIP通道(任务队列不同)
6. Web Worker中的setTimeout会阻塞主线程吗?
- 原理:像分店后厨,独立运作互不影响
7. 微任务会饿死宏任务吗?
- 案例:无限递归Promise.then会让页面卡死
8. 点击事件和setTimeout谁先执行?
- 规则:像叫号系统,先到先得(取决于触发时机)
9. 为什么async函数比setTimeout优先?
- 本质:async返回Promise,属于微任务
增强演示:事件循环运转图 🎡
三维运转模型
┌───────────────────────┐
│ 宏任务队列 │ ← click事件
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ 调用栈(大堂) │ ← 正在执行脚本
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ 微任务队列 │ ← Promise.then
└──────────┬────────────┘
│
┌──────────▼────────────┐
│ 渲染管道(上菜窗口) │ ← requestAnimationFrame
└───────────────────────┘
动态演示工具
// 在浏览器控制台运行观察
function 压力测试() {
setTimeout(() => console.log('宏任务1'), 0);
Promise.resolve().then(() => {
console.log('微任务1');
Promise.resolve().then(() => console.log('嵌套微任务'));
});
requestAnimationFrame(() => console.log('动画帧回调'));
}
压力测试();
// 输出顺序:微任务1 → 嵌套微任务 → 动画帧回调 → 宏任务1
实战进阶:高并发涮肉术 🥢
案例1:避免汤底烧干(防止阻塞)
// 错误示范:同步计算阻塞
function 切肉() {
let i = 0;
while(i++ < 1000000000); // 同步阻塞
}
// 正确做法:分片处理
function 高效切肉() {
function 切片(start) {
if(start >= 1000000) return;
requestIdleCallback(() => {
while(start < start + 1000) { /* 处理切片 */ }
切片(start + 1000);
});
}
切片(0);
}
案例2:智能上菜系统(优先级控制)
class 任务调度器 {
constructor() {
this.微任务队列 = [];
this.宏任务队列 = [];
}
加菜(任务, 类型) {
(类型 === '微' ? this.微任务队列 : this.宏任务队列).push(任务);
}
上菜() {
while(this.微任务队列.length) {
const 任务 = this.微任务队列.shift();
任务();
}
if(this.宏任务队列.length) {
const 任务 = this.宏任务队列.shift();
setTimeout(任务, 0);
}
}
}
灵魂暴击10连问 💥
- process.nextTick与Promise.then的优先级差异?
- 如何实现一个微任务polyfill?
- Node.js与浏览器事件循环的核心区别?
- requestAnimationFrame属于哪类任务?
- 为什么MutationObserver是微任务?
- 如何检测主线程阻塞?
- 宏任务之间的执行顺序规则?
- Web Worker如何影响事件循环?
- 如何实现任务优先级反转?
- 事件循环与垃圾回收的关系?
面试反杀菜谱 🥗
当面试官深入追问时,展示这道"硬菜":
console.log('锅底');
setTimeout(() => {
console.log('香菜');
Promise.resolve().then(() => console.log('葱花'));
}, 0);
Promise.resolve().then(() => {
console.log('香油');
setTimeout(() => console.log('蒜末'), 0);
});
requestAnimationFrame(() => console.log('加汤'));
// 正确输出顺序:锅底 → 香油 → 加汤 → 香菜 → 葱花 → 蒜末
防忘涮肉口诀 🧠
- 同步任务马上涮
- 微任务像小料台
- 宏任务等叫号
- 渲染前要加汤
- 嵌套任务像拼盘
免责声明:本文部分内容由AI生成,技术细节仅供参考。实际开发请以各运行时环境为准。
下期预告:CSS布局原理:从俄罗斯方块到乐高积木的排列艺术