🔥 彻底搞懂 JavaScript 运行机制:宏任务与微任务的“爱恨情仇”

0 阅读5分钟

🔥 彻底搞懂 JavaScript 运行机制:宏任务与微任务的“爱恨情仇”

前言

在前端面试中,有这样一道必考题:“请说一下 JavaScript 的事件循环(Event Loop)机制,什么是宏任务和微任务?” 很多同学在面对满屏的 setTimeoutPromise 时,往往会被绕得晕头转向,输出结果全靠猜。

今天,我们就用最通俗易懂的生活化比喻,由简入深地带你彻底打通 JS 运行机制的任督二脉!🚀


1. 为什么 JS 是单线程的?

首先,我们要明确一个大前提:JavaScript 是一门单线程语言

💡 通俗理解: 假设 JS 引擎是一个厨房,单线程意味着这个厨房里只有一个主厨。所有的菜(代码)都必须由这个主厨一道一道地炒,不能同时炒两道菜。

为什么不设计成多线程呢? 因为 JS 最初是为浏览器设计的,主要用来操作 DOM。如果同时有两个线程,线程 A 要删除一个节点,线程 B 要修改这个节点,浏览器该听谁的?为了避免这种复杂的同步问题,JS 干脆被设计成了单线程。


2. 同步与异步:主厨与帮厨

既然只有一个主厨,那如果有一道菜(比如炖汤)需要熬 2 个小时,主厨难道要一直傻站着等吗?那后面的客人岂不是要饿死?

为了解决这个问题,JS 引入了同步异步的概念:

  • 同步任务:马上就能完成的活儿,比如切菜、炒个青菜。主厨自己顺手就干了。
  • 异步任务:需要花时间等待的活儿,比如炖汤(网络请求 Ajax)、定时器(setTimeout)。

主厨遇到异步任务时,会把它交给帮厨(浏览器的其他线程,如定时器线程、网络请求线程)去盯着,自己则继续去做后面的同步任务。等帮厨把汤炖好了,就会把这道菜放到出菜区(任务队列),主厨忙完手头的活儿,就会去出菜区看看有没有做好的菜,拿过来继续处理。


3. 宏任务(Macrotask)与微任务(Microtask)

随着前端技术的发展,出菜区(任务队列)里的任务也变得复杂起来。为了区分轻重缓急,JS 将异步任务分为了两类:宏任务微任务

🏦 银行办业务的绝佳比喻

我们可以把 Event Loop 想象成去银行办业务

  1. 宏任务(Macro-task):相当于排队办业务的人

    • 常见的宏任务:script (整体代码)、setTimeoutsetIntervalI/OUI 渲染
    • 规则:每次柜员(主线程)叫号,只能处理一个人的业务(执行一个宏任务)。
  2. 微任务(Micro-task):相当于当前正在办业务的人,临时加办的额外需求

    • 常见的微任务:Promise.then/catch/finallyMutationObserverprocess.nextTick (Node.js)。
    • 规则:比如你正在柜台存钱(当前宏任务),存完后你突然说:“哎,我再顺便买个理财吧!”(产生微任务)。柜员会立刻帮你把理财也办了,而不是让你重新去后面排队。只有把你所有的临时需求(微任务)都办完,柜员才会叫下一个排队的人(下一个宏任务)。

4. 核心:Event Loop 执行顺序

记住下面这个黄金法则,你就能秒杀所有执行顺序的题目:

  1. 执行同步代码:这属于宏任务(也就是第一个排队的人)。
  2. 清空微任务队列:同步代码执行完后,检查有没有微任务。如果有,全部执行完毕(满足当前客户的所有临时需求)。
  3. UI 渲染:浏览器会在此时进行视图更新(如果有需要)。
  4. 执行下一个宏任务:从宏任务队列中取出一个任务执行(叫下一个排队的人)。
  5. 循环往复:回到第 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. 同步代码结束');

🕵️‍♂️ 逐行解析:

  1. 执行 console.log('1')打印 1
  2. 遇到 setTimeout,把它交给帮厨(定时器线程),0秒后帮厨把它放入宏任务队列
  3. 遇到 new Promise,注意!Promise 的构造函数是同步执行的,所以立即执行 console.log('3')打印 3
  4. 执行 resolve(),触发 .then,这是一个微任务,把它放入微任务队列
  5. 执行 console.log('5')打印 5
  6. 第一轮宏任务(同步代码)执行完毕!
  7. 主厨去检查微任务队列,发现有一个 .then,立刻执行它,打印 4
  8. 微任务队列清空完毕。
  9. 主厨去检查宏任务队列,发现有一个 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 等),每次宏任务结束后,必须全部清空
  • 执行顺序:同步代码(宏任务) -> 清空微任务 -> 渲染 -> 下一个宏任务 -> 清空微任务...

掌握了这些,以后再遇到复杂的异步代码,只需要在脑海里画出“宏任务”和“微任务”两个队列,一步步推导,绝对不会出错!

如果这篇文章让你恍然大悟,别忘了点赞收藏哦~ 👍✨