🎓 作者简介 : 前端领域优质创作者
🚪 资源导航: 传送门=>
🎬 个人主页: 江城开朗的豌豆
🌐 个人网站: 江城开朗的豌豆 🌍
📧 个人邮箱: [email protected] 📩
💬 个人微信: y_t_t_t_ 📱
📌 座 右 铭: 生活就像心电图,一帆风顺就证明你挂了。 💔
👥 QQ群: 906392632 (前端技术交流群) 💬
最近在面试前端工程师时,发现很多候选人对事件循环(Event Loop)的理解只停留在概念层面。这让他想起自己刚学习JavaScript时,也曾被setTimeout的"怪异"行为困扰。今天,我们就用一个厨房的比喻,彻底搞懂这个JavaScript核心机制。
厨房里的异步工作流
想象开了一家小餐馆,厨房的工作流程完美诠释了事件循环的原理:
- 厨师(主线程) :一次只能做一道菜(同步任务)
- 订单队列(任务队列) :新订单按顺序排队等待
- 定时器(计时器) :需要等待的菜品(如炖汤)
- 服务员(Web API) :处理外卖订单等异步任务
javascript
console.log('开始营业'); // 同步任务 - 开门营业
setTimeout(() => {
console.log('炖汤完成'); // 异步回调 - 需要时间的菜品
}, 2000);
Promise.resolve().then(() => {
console.log('凉菜备好'); // 微任务 - 快速出餐的小菜
});
console.log('接待第一位客人'); // 同步任务 - 立即处理
输出顺序是:
javascript
开始营业
接待第一位客人
凉菜备好
炖汤完成
事件循环的三层结构
画出了事件循环的核心组成:
- 调用栈(Call Stack) :正在执行的同步代码
- 任务队列(Task Queue) :setTimeout、DOM事件等宏任务
- 微任务队列(Microtask Queue) :Promise、MutationObserver等
javascript
console.log('脚本开始'); // 1. 同步任务
setTimeout(() => {
console.log('setTimeout'); // 4. 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('Promise'); // 3. 微任务
});
console.log('脚本结束'); // 2. 同步任务
真实场景中的执行顺序
在开发时遇到一个典型问题:
javascript
document.querySelector('#btn').addEventListener('click', () => {
console.log('点击事件开始'); // 宏任务
Promise.resolve().then(() => {
console.log('Promise微任务');
});
setTimeout(() => {
console.log('setTimeout宏任务');
}, 0);
console.log('点击事件结束');
});
点击按钮后的输出顺序:
javascript
点击事件开始
点击事件结束
Promise微任务
setTimeout宏任务
常见误区解析
误区1:setTimeout(fn, 0)会立即执行
曾经以为:
javascript
setTimeout(() => {
console.log('杨涛认为这会立即执行');
}, 0);
实际上,即使延迟为0,它仍然要等所有同步代码和微任务执行完后才会运行。
误区2:Promise比setTimeout快
看这个复杂例子:
javascript
setTimeout(() => {
console.log('宏任务1');
}, 0);
Promise.resolve().then(() => {
console.log('微任务1');
setTimeout(() => {
console.log('宏任务2');
}, 0);
});
Promise.resolve().then(() => {
console.log('微任务2');
});
输出顺序:
javascript
微任务1
微任务2
宏任务1
宏任务2
事件循环的浏览器实现
通过Chrome DevTools验证事件循环:
- 在Sources面板添加断点
- 在Console执行异步代码
- 使用Performance面板记录执行过程
javascript
function test() {
console.log('同步代码');
setTimeout(() => {
console.log('宏任务');
debugger;
}, 1000);
Promise.resolve().then(() => {
console.log('微任务');
});
}
Node.js与浏览器的事件循环差异
在Node服务端开发时发现:
javascript
setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise1');
});
}, 0);
setTimeout(() => {
console.log('timer2');
}, 0);
浏览器输出:
javascript
timer1
promise1
timer2
Node 11+版本输出相同,但旧版本可能不同。
实战应用技巧
1. 长任务分解
优化页面卡顿的方案:
javascript
// 阻塞主线程的繁重任务
function processBigData() {
// 分解为小块
const chunkSize = 1000;
let i = 0;
function processChunk() {
while (i < data.length && i % chunkSize !== 0) {
// 处理数据...
i++;
}
if (i < data.length) {
setTimeout(processChunk, 0); // 让出主线程
}
}
processChunk();
}
2. 优先级控制
javascript
// 高优先级任务用Promise
function urgentTask() {
Promise.resolve().then(doUrgentWork);
}
// 低优先级任务用setTimeout
function normalTask() {
setTimeout(doNormalWork, 0);
}
总结
理解事件循环后,异步代码质量显著提升。关键要点:
- 同步任务立即执行,阻塞调用栈
- 微任务在当前宏任务结束后立即执行
- 宏任务在下次事件循环执行
- 渲染发生在宏任务之间
"事件循环就像餐厅运营,只有理解整个流程,才能高效协调各项工作。"这是在团队分享时的总结。掌握这一机制,你就能写出更高效的JavaScript代码。