🔥 彻底搞懂 JavaScript 运行机制:宏任务与微任务的“爱恨情仇”
前言
在前端面试中,有这样一道必考题:“请说一下 JavaScript 的事件循环(Event Loop)机制,什么是宏任务和微任务?”
很多同学在面对满屏的 setTimeout 和 Promise 时,往往会被绕得晕头转向,输出结果全靠猜。
今天,我们就用最通俗易懂的生活化比喻,由简入深地带你彻底打通 JS 运行机制的任督二脉!🚀
1. 为什么 JS 是单线程的?
首先,我们要明确一个大前提:JavaScript 是一门单线程语言。
💡 通俗理解: 假设 JS 引擎是一个厨房,单线程意味着这个厨房里只有一个主厨。所有的菜(代码)都必须由这个主厨一道一道地炒,不能同时炒两道菜。
为什么不设计成多线程呢? 因为 JS 最初是为浏览器设计的,主要用来操作 DOM。如果同时有两个线程,线程 A 要删除一个节点,线程 B 要修改这个节点,浏览器该听谁的?为了避免这种复杂的同步问题,JS 干脆被设计成了单线程。
2. 同步与异步:主厨与帮厨
既然只有一个主厨,那如果有一道菜(比如炖汤)需要熬 2 个小时,主厨难道要一直傻站着等吗?那后面的客人岂不是要饿死?
为了解决这个问题,JS 引入了同步和异步的概念:
- 同步任务:马上就能完成的活儿,比如切菜、炒个青菜。主厨自己顺手就干了。
- 异步任务:需要花时间等待的活儿,比如炖汤(网络请求
Ajax)、定时器(setTimeout)。
主厨遇到异步任务时,会把它交给帮厨(浏览器的其他线程,如定时器线程、网络请求线程)去盯着,自己则继续去做后面的同步任务。等帮厨把汤炖好了,就会把这道菜放到出菜区(任务队列),主厨忙完手头的活儿,就会去出菜区看看有没有做好的菜,拿过来继续处理。
3. 宏任务(Macrotask)与微任务(Microtask)
随着前端技术的发展,出菜区(任务队列)里的任务也变得复杂起来。为了区分轻重缓急,JS 将异步任务分为了两类:宏任务和微任务。
🏦 银行办业务的绝佳比喻
我们可以把 Event Loop 想象成去银行办业务:
-
宏任务(Macro-task):相当于排队办业务的人。
- 常见的宏任务:
script(整体代码)、setTimeout、setInterval、I/O、UI 渲染。 - 规则:每次柜员(主线程)叫号,只能处理一个人的业务(执行一个宏任务)。
- 常见的宏任务:
-
微任务(Micro-task):相当于当前正在办业务的人,临时加办的额外需求。
- 常见的微任务:
Promise.then/catch/finally、MutationObserver、process.nextTick(Node.js)。 - 规则:比如你正在柜台存钱(当前宏任务),存完后你突然说:“哎,我再顺便买个理财吧!”(产生微任务)。柜员会立刻帮你把理财也办了,而不是让你重新去后面排队。只有把你所有的临时需求(微任务)都办完,柜员才会叫下一个排队的人(下一个宏任务)。
- 常见的微任务:
4. 核心:Event Loop 执行顺序
记住下面这个黄金法则,你就能秒杀所有执行顺序的题目:
- 执行同步代码:这属于宏任务(也就是第一个排队的人)。
- 清空微任务队列:同步代码执行完后,检查有没有微任务。如果有,全部执行完毕(满足当前客户的所有临时需求)。
- UI 渲染:浏览器会在此时进行视图更新(如果有需要)。
- 执行下一个宏任务:从宏任务队列中取出一个任务执行(叫下一个排队的人)。
- 循环往复:回到第 2 步。
⚠️ 重点提醒:微任务是一次性清空(执行完队列里所有的),而宏任务是一次只执行一个!
5. 终极实战:你能做对这道面试题吗?
理论学完了,我们来实战演练一下这道经典的面试题:
console.log('1. 同步代码开始');
setTimeout(() => {
console.log('2. setTimeout 宏任务');
}, 0);
new Promise((resolve) => {
console.log('3. Promise 构造函数是同步的');
resolve();
}).then(() => {
console.log('4. Promise.then 微任务');
});
console.log('5. 同步代码结束');
🕵️♂️ 逐行解析:
- 执行
console.log('1'),打印 1。 - 遇到
setTimeout,把它交给帮厨(定时器线程),0秒后帮厨把它放入宏任务队列。 - 遇到
new Promise,注意!Promise 的构造函数是同步执行的,所以立即执行console.log('3'),打印 3。 - 执行
resolve(),触发.then,这是一个微任务,把它放入微任务队列。 - 执行
console.log('5'),打印 5。 - 第一轮宏任务(同步代码)执行完毕!
- 主厨去检查微任务队列,发现有一个
.then,立刻执行它,打印 4。 - 微任务队列清空完毕。
- 主厨去检查宏任务队列,发现有一个
setTimeout,拿出来执行,打印 2。
🎉 最终输出结果:
1 -> 3 -> 5 -> 4 -> 2
6. 进阶:微任务里嵌套微任务怎么办?
如果在执行微任务的过程中,又产生了新的微任务呢?
Promise.resolve().then(() => {
console.log('微任务 1');
Promise.resolve().then(() => {
console.log('微任务 2');
});
});
setTimeout(() => {
console.log('宏任务 1');
}, 0);
答案是:微任务 1 -> 微任务 2 -> 宏任务 1。 还记得银行的比喻吗?只要你还在柜台前(微任务队列没清空),你提出的所有合理新需求(新产生的微任务),柜员都会一直帮你办完,直到你彻底没事了,才会叫下一个排队的人(宏任务)。
总结
- JS 是单线程的,通过 Event Loop 机制处理异步任务。
- 宏任务:排队办业务的人(
setTimeout等),每次只执行一个。 - 微任务:当前客户的临时需求(
Promise.then等),每次宏任务结束后,必须全部清空。 - 执行顺序:同步代码(宏任务) -> 清空微任务 -> 渲染 -> 下一个宏任务 -> 清空微任务...
掌握了这些,以后再遇到复杂的异步代码,只需要在脑海里画出“宏任务”和“微任务”两个队列,一步步推导,绝对不会出错!
如果这篇文章让你恍然大悟,别忘了点赞收藏哦~ 👍✨