在上一篇文章中,我们聊了闭包这个"随身背包"。闭包让函数拥有了跨越时空的记忆,但这也带来了一个隐患------如果闭包里的异步操作(比如定时器、网络请求)处理不好,你的网页可能就会卡得像幻灯片。
这就引出了 JavaScript 世界里最大的"玄学":单线程的 JS,到底是怎么做到一边发网络请求、一边响应用户点击,还能保证页面丝滑不卡顿的?
答案就是:事件循环(Event Loop) 。
在面试界,Event Loop 是绝对的"地狱级 BOSS"。面试官只要丢出一段包含 setTimeout 和 Promise 的代码,问你"打印顺序是什么",就能让无数候选人当场 CPU 烧毁。但其实,Event Loop 根本不是什么玄学,它就像是一个极其严苛的"时间管理大师"和"插队狂魔"。
今天,咱们继续泡杯咖啡,用最接地气的方式,把宏任务、微任务、Promise 的执行顺序扒个底朝天!
一、 为什么需要 Event Loop?一个"餐厅服务员"的故事
想象一下,JavaScript 引擎就是餐厅里唯一的一个服务员 (单线程)。
如果服务员是个死心眼,客人点了一份需要炖 3 个小时的佛跳墙,他就傻站在厨房门口等 3 个小时。在这期间,其他客人点菜、催单、结账,他全都不管------这就叫 "主线程阻塞" ,网页直接卡死。
为了让餐厅运转下去,聪明的老板(浏览器)给服务员定了一套规矩,这就是 Event Loop(事件循环) :
- 客人点单(同步任务),服务员立刻去厨房端菜(执行同步代码)。
- 如果客人点了佛跳墙(异步任务,如定时器、Ajax),服务员绝不傻等,而是把单子贴在厨房的"等待黑板"(任务队列)上,然后转身去服务下一位客人。
- 等客人手头的事都忙完了(同步代码执行完,调用栈清空),服务员才会去看黑板,把做好的菜端出来。
二、 宏任务与微任务:VIP 插队机制
如果所有的异步任务都按先来后到排队,那网页的渲染和状态更新就会很慢。于是,老板引入了VIP 插队机制,把任务分成了两类:
1. 宏任务(Macrotask):普通排队的客人
包括:setTimeout、setInterval、I/O 操作、UI 渲染等。
它们是常规的异步任务,必须等当前这一轮服务彻底结束,才能轮到它们。
2. 微任务(Microtask):拥有最高优先级的 VIP
包括:Promise.then/catch/finally、MutationObserver、queueMicrotask 等。
它们享有 "特权插队权" 。只要当前一个宏任务刚执行完,服务员必须立刻、马上把微任务队列里的活儿全部干完,干到清空为止!然后才能去管下一个宏任务。
三、 黄金法则:Event Loop 的"四步走"
把上面的故事翻译成代码执行流程,你只需要记住这四步死循环:
- 执行同步代码:把调用栈(Call Stack)里的活儿全干完。
- 清空微任务队列 :只要栈空了,立刻检查微任务队列,一口气全部执行完(如果执行微任务时又产生了新的微任务,继续清空,直到彻底没有)。
- 尝试 UI 渲染:浏览器看看需不需要更新页面(这一步不是每次都做,看浏览器心情和帧率)。
- 取出一个宏任务执行 :从宏任务队列里拿一个出来执行,执行完后,再次回到第 2 步!
四、 实战演练:面试官最爱的"夺命连环题"
光说不练假把式,我们来看一段经典代码,用刚才的"四步走"法则来盘它:
javascript
编辑
javascript
console.log('1. 同步代码');
setTimeout(() => {
console.log('4. setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('3. Promise 微任务');
});
console.log('2. 同步代码结束');
大脑 CPU 开始运转:
- 执行同步代码 :先打印
1. 同步代码,接着遇到setTimeout,把它踢进宏任务队列。接着遇到Promise,把.then踢进微任务队列。最后打印2. 同步代码结束。 - 清空微任务队列 :同步代码完了!立刻检查微任务队列,发现有个 Promise,立刻执行,打印
3. Promise 微任务。 - 取出宏任务 :微任务清空了,去宏任务队列把
setTimeout拿出来执行,打印4. setTimeout。
最终输出顺序 :1. 同步代码 -> 2. 同步代码结束 -> 3. Promise 微任务 -> 4. setTimeout。
是不是瞬间豁然开朗?
五、 进阶陷阱:async/await 到底是个啥?
很多新手看到 async/await 就发懵。其实,async/await 就是 Promise 的终极语法糖!
当你写下 await 时,JavaScript 引擎会做两件事:
- 先执行
await后面的表达式。 - 把
await下面的代码,包装成一个微任务,扔进微任务队列,然后跳出当前函数,去执行外面的同步代码。
所以,await 本质上就是"让出主线程,稍后以微任务的身份继续执行"。
六、 总结
Event Loop 并不玄学,它只是 JavaScript 为了在"单线程"的先天残疾下,依然能优雅地处理高并发和异步操作而发明的一套 "调度算法" 。
记住这句口诀:同步先执行,微任务插队全清空,宏任务排队挨个来!
掌握了事件循环,你就能精准预测每一行异步代码的执行时机,写出丝滑流畅、绝不卡顿的网页。