朋友们,想象一下你是个忙碌的咖啡师👨🍳,既要接单又要做咖啡,但只有一双手!这就是JavaScript的处境——单线程执行,一次只能做一件事。那么它如何同时处理点击事件、网络请求和动画呢?答案就是事件循环(Event Loop) 这个幕后魔术师!
🧩 为什么需要Event Loop?
JavaScript生来就是单线程的(就像咖啡师只有一双手)。如果所有任务都同步执行:
// 同步地狱示例
downloadHugeFile(); // 卡住10秒
handleUserClick(); // 用户疯狂点击无响应
animateElement(); // 动画冻结
大家都知道我们前端是十分注重用户体验的,一但用户会看到"页面无响应"的提示😱!那么会直接关闭我们的网页,影响我们网页的访问量,Event Loop的诞生就是为了解决这个问题——让耗时不阻塞关键操作,就像咖啡师让磨豆机自己工作,先去服务顾客。
🎭 Event Loop 剧场:三幕剧
通过我们今天的代码案例,揭秘事件循环的三幕大戏:
每一个script就是一个宏任务,里面有同步任务和异步任务,异步任务又分为宏任务和微任务,当同步任务执行完就开始执行所有异步任务的微任务,执行完后才开始执行其它宏任务,所以微任务是异步任务里面的vip
三者关系如下图,执行完毕指的是所有的同步任务执行完毕
第一幕:同步任务闪电战 ⚡
<script>
console.log('script start'); // 【同步】立即执行
console.log('script end'); // 【同步】立即执行
</script>
同步代码像闪电侠🏃♂️,总是最先冲过终点线。执行顺序:
script start → script end
第二幕:微任务插队优先证 🎫
<script>
Promise.resolve().then(() => {
console.log('promise1'); // 【微任务】插队执行!
});
</script>
微任务就像VIP客户,总是插队到下一个宏任务之前,但依旧在同步任务之后
console.log('同步Start');
Promise.resolve().then(() => console.log('First Promise'));
console.log('同步End');
// 输出:
// 同步Start → 同步End → First Promise
第三幕:宏任务耐心排队 🚶♂️🚶♀️
<script>
setTimeout(() => {
console.log('setTimeout'); // 【宏任务】老实排队
}, 0);
</script>
宏任务像普通顾客,规规矩矩排队,只有当所有的同步任务和微任务执行完,才能执行下一个宏任务,每一个宏任务有自己的执行上下文,又会按照上述event loop重复步骤
console.log('script start');
Promise.resolve().then(() => console.log('First Promise'));
setTimeout(() => {
console.log('setTimeout satrt'); // 【宏任务】老实排队
Promise.resolve().then(() => console.log('Second Promise')); // 【微任务】优先执行
console.log('setTimeout end');
}, 0);
setTimeout(() => {
console.log('setTimeout2'); // 【宏任务】老实排队
}, 0)
console.log('script end');
可以看到第一个定时器,创建一个属于自己的执行上下文,里面有属于自己的同步任务,微任务,当然也可以有宏任务
setTimeout(() => console.log('下一个宏任务'));
setTimeout(() => console.log('下下一个宏任务'));
// 输出:
// 下一个宏任务 → 下下一个宏任务
🎪 任务类型大揭秘
🟢 微任务(VIP通道)
- `promise.then()` → 异步之王
- `MutationObserver` → DOM侦探
- `queueMicrotask` → 微任务直通车
- `process.nextTick` → Node.js特快
MutationObserver 现场秀 👓
<script>
const target = document.createElement('div');
const observer = new MutationObserver(() => {
console.log('微任务: MutationObserver'); // DOM变更立即捕获!
});
target.setAttribute('class', 'test'); // 触发1次
target.appendChild(document.createElement('span')); // 再触发?
target.setAttribute('style', 'color:red'); // 又触发?
</script>
神奇的是:多次DOM修改只触发一次回调!因为浏览器会把同事件循环内的DOM变更打包处理。
其实就是批处理了,类似于useState解构出来的响应式改变状态的函数,如果多次setState(state + 1)也会批处理为一次,页面只+1
queueMicrotask 直通车 🚀
<script>
console.log('同步');
queueMicrotask(() => console.log('微任务: queueMicrotask'));
console.log('同步结束');
</script>
输出永远是:
同步 → 同步结束 → 微任务: queueMicrotask
🔵 宏任务(普通通道)
- `script`(整体代码)→ 开场大戏
- `setTimeout/setInterval` → 定时器代表
- `I/O操作` → 文件/网络请求
- `UI渲染` → 重绘重排
🎯 Node.js特别场:process.nextTick
console.log('Start');
Promise.resolve().then(() => console.log('Promise Resolved'));
process.nextTick(() => console.log('Process Next Tick'));
console.log('end');
/* 输出顺序:
Start
end
Process Next Tick → Node.js微任务之王!
Promise Resolved
*/
在Node.js中,process.nextTick优先级甚至高于Promise,是真正的微任务VIP中P!
🌈 终极挑战:混合任务大乱斗
console.log('同步Start');
const promise1 = Promise.resolve('First Promise');
const promise2 = Promise.resolve('Second Promise');
const pormise3 = new Promise(resolve => {
// 同步的,所以先打印promise3,然后打印同步End
// 只有.then()才是异步的,是微任务
console.log('promise3');
resolve('Third Promise');
})
promise1.then(value => {
console.log(value);
})
promise2.then(value => {
console.log(value);
})
pormise3.then(value => {
console.log(value);
})
setTimeout(() => {
console.log('下一个宏任务');
const promise4 = Promise.resolve('Fourth Promise');
promise4.then(value => {
console.log(value);
})
}, 0)
setTimeout(() => {
console.log('下下一个宏任务');
}, 0)
console.log('同步End');
关键规律:
- 同步代码永远最先执行
- 微任务清空前,下一个宏任务休想启动
- 每个宏任务都自带微任务队列
💡 为什么这样设计?
想象一个场景:你修改了DOM后想立即获取元素尺寸:
button.style.width = '200px';
console.log(button.offsetWidth); // 需要最新值!
如果这时UI渲染(宏任务)插队,获取的将是旧值!并且会引起新的重排,消耗新能,因此:
- 同步代码修改DOM
- 微任务中可获取未渲染的DOM变更
- 宏任务执行前先执行UI渲染
这就是为什么MutationObserver被设计为微任务——在渲染前捕获DOM变更!
🚀 总结:Event Loop 三定律
- 同步优先:主线程代码一路狂奔到底
- 微次之:Promise、MutationObserver等紧随其后
- 宏最后:setTimeout、渲染等压轴出场
记住这个永恒公式:
同步 → 微任务 → 渲染 → 宏任务 → (微任务 again)
最后附表,这里展示了一下常见的宏任务和微任务
🔮 JavaScript 任务类型速查表
| 任务类型 | 具体API | 执行时机 | 环境 | 趣味解读 |
|---|---|---|---|---|
| 🔴 宏任务 | script标签 | 最先执行 | 通用 | "程序入口,开胃菜" |
setTimeout | 下一轮循环 | 通用 | "闹钟叫醒的拖延症患者" ⏰ | |
setInterval | 周期性执行 | 通用 | "永动机(直到clear)" ♾️ | |
| I/O操作 | 回调阶段 | 通用 | "快递小哥送货上门" 📦 | |
| UI渲染 | 微任务后 | 浏览器 | "舞台灯光师" 💡 | |
| 事件监听 | 事件触发时 | 浏览器 | "随叫随到的服务员" 🛎️ | |
| 🟢 微任务 | Promise.then | 同步结束立即执行 | 通用 | "VIP快速通道" 💎 |
queueMicrotask | 同步结束立即执行 | 浏览器 | "微任务直通车" 🚄 | |
MutationObserver | DOM变更后 | 浏览器 | "DOM私家侦探" 🔍 | |
process.nextTick | 当前循环末尾 | Node.js | "Node插队王" 👑 | |
Promise.catch | 同步结束立即执行 | 通用 | "错误处理急先锋" 🚨 |