用 “餐厅模型” 吃透事件循环:同步、宏任务、微任务谁先执行?

深入浅出JavaScript事件循环:宏任务、微任务与你的面试官

✨ 为什么JavaScript是"单线程非阻塞"的?

单线程语言:

因为在浏览器中,需要对各种的DOM操作;

当JS是多线程的话,如果有两个线程同时对同一个DOM进行操作,一个是在这个DOM上绑定事件,另外一个是删除该DOM,此时就会产生歧义;

因此为了保证这种事情不会发生,所以JS以单线程来执行代码,保证了一致性

JS非阻塞:

当JS代码从上往下执行,遇到需要进行一项异步任务的时候, 主线程会挂起这个任务,继续往下执行代码,然后在异步任务返回结果的时候再根据一定规则去执行

你有没有想过,为什么JavaScript在浏览器里能"一手遮天"?答案就是:它是一门单线程的语言。这就像一个餐厅,只有一个厨师(主线程)在工作。这个厨师很厉害,他能处理所有的点餐、炒菜、上菜等任务。如果同时来了两个客人,一个说"我要把桌子擦干净",另一个说"我要把桌子搬走",如果厨师是多线程的,他可能就会陷入"擦一半搬一半"的尴尬境地,甚至把桌子擦没了,客人还怎么吃饭?为了避免这种"左右互搏"的混乱局面,JavaScript从诞生之初就被设计成单线程,保证了操作的一致性和顺序性。

但是,单线程就意味着效率低下吗?比如,厨师在炒一道需要炖很久的菜(比如红烧肉),难道他就要一直盯着锅,什么都不干吗?当然不是!这就是JavaScript"非阻塞"的精髓所在。当遇到像炖红烧肉(异步任务,比如网络请求、定时器)这种耗时任务时,聪明的厨师会把它交给"后厨小弟"(浏览器或Node.js的API)去处理,自己则继续去炒其他的菜(执行后续的同步代码)。等红烧肉炖好了,"后厨小弟"会通知厨师,然后厨师再根据一定的规则(事件循环)来处理这道菜的后续工作。

所以,JavaScript的"单线程非阻塞"特性,就像一个高效的餐厅:一个主厨负责统筹安排,遇到耗时任务就交给帮手,自己绝不干等,从而保证了餐厅的持续高效运转。那么,这个"一定规则"到底是什么呢?它就是我们今天要深入探讨的------事件循环(Event Loop)

🔄 事件循环机制大揭秘

想象一下,你来到一家热门餐厅,门口排着长队。这家餐厅的运营模式,就和JavaScript的事件循环机制有异曲同工之妙。

首先,餐厅有一个主厨(JS引擎) ,他手头有一个点餐板(执行栈)。所有客人点的主菜(同步任务)都会直接写在点餐板上,主厨会一道一道地立即制作。这些菜品都是立等可取的,比如"来份拍黄瓜",主厨立马就给你拍好了。

但是,有些菜品需要时间制作,比如"来份红烧肉",或者"来杯现榨果汁"。这些不能立等可取的任务,主厨会把它们交给后厨小弟(Web APIs/Node.js APIs) 去处理,并把这些任务记录在任务表(Task Table) 上。后厨小弟处理完后,并不会直接把菜端给主厨,而是把做好的菜放到一个传菜口(任务队列)

这个传菜口可不是只有一个,它分成了两个区域:

  • 宏任务队列(MacroTask Queue) :这里放的是那些"大菜",比如红烧肉(setTimeout, setInterval)、烤鸭(I/O操作)、或者一桌子的宴席(UI渲染)。这些任务通常耗时较长,而且数量可能比较多。
  • 微任务队列(MicroTask Queue) :这里放的是那些"小吃",比如餐前小点心(Promise.then().catch().finally())、或者一些需要立即处理的"加急件"(MutationObserver)。这些任务通常执行速度快,优先级相对较高。

那么,主厨是如何从传菜口取菜的呢?这就是循环(Loop) 的关键所在了!

  1. 主厨优先处理点餐板上的所有主菜(执行栈中的所有同步任务)。他会一道接一道地做,直到点餐板空了为止。
  2. 点餐板空了之后,主厨会先去微任务队列看看有没有"小吃"。如果微任务队列里有,他会把所有的小吃都取出来,一口气全部处理完。因为这些都是"加急件",必须优先处理。
  3. 微任务队列清空后,主厨才会去宏任务队列取一道"大菜"来处理 。注意,每次循环只会从宏任务队列中取一个任务来执行。
  4. 处理完这道"大菜"后,主厨会再次回到第2步,检查微任务队列。因为在处理"大菜"的过程中,可能会产生新的"小吃"。
  5. 如此循环往复,直到两个队列都清空,主厨才能休息。

这个过程,就是JavaScript事件循环的完整机制。它保证了JavaScript在单线程环境下,既能高效处理同步任务,又能异步处理耗时任务,并且对不同类型的异步任务进行了优先级管理。

JS引擎

  • 执行栈(Call Stack):所有同步任务都在主线程上执行,形成一个执行栈。当函数被调用时,会被推入栈中;当函数执行完毕,会被弹出栈。
  • 任务队列(Task Queue) :用于存放异步任务的回调函数。分为宏任务队列和微任务队列。
    • 宏任务(MacroTask)setTimeout, setInterval, I/O, UI Rendering等。
    • 微任务(MicroTask)Promise.then().catch().finally(), MutationObserver, process.nextTick (Node.js环境)等。

理解了这个"餐厅模型",你是不是对事件循环有了更直观的认识呢?接下来,我们通过一个实际的代码例子,来看看这个机制是如何运作的。

🔧 代码实战:宏任务与微任务的舞蹈

理论知识讲了一大堆,是不是有点晕?别急,我们来点实际的!下面这段代码,就是面试官最喜欢拿来"考考你"的经典案例。我们用它来模拟一下宏任务和微任务在事件循环中的"舞蹈"过程。

javascript 复制代码
// 同步任务
console.log("首次同步任务开始");

// 异步任务 (宏任务)
setTimeout(() => {
  console.log("setTimeout 1");
  new Promise((resolve) => {
    console.log("promise1");
    resolve();
  }).then(() => {
    console.log("Promise then 1");
  });
}, 1000);

// 同步任务
console.log("首次同步任务结束");

// 异步任务 (微任务)
new Promise((resolve) => {
  console.log("promise2");
  resolve();
}).then(() => {
  console.log("Promise then 2");
});

这段代码的输出顺序是什么呢?别急着说答案,我们一步步来"拆解"它。

第一步:执行同步任务

就像餐厅的主厨,他会先把点餐板上的"拍黄瓜"们(同步任务)全部处理掉。所以,最先输出的是:

复制代码
首次同步任务开始
首次同步任务结束

在执行这两个console.log的同时,setTimeoutPromise这两个异步任务也被"登记"了。setTimeout被扔进了宏任务队列,而Promisethen方法(注意,是then方法里的回调,而不是Promise本身)被扔进了微任务队列。

第二步:处理微任务队列

同步任务执行完毕,执行栈空了。这时候,主厨会先去微任务队列看看有没有"小吃"。嘿,果然有!promise2Promise then 2正在那里等着呢。所以,它们紧接着被执行:

javascript 复制代码
promise2
Promise then 2

第三步:处理宏任务队列

微任务队列清空了,主厨终于可以去宏任务队列里取"大菜"了。此时,setTimeout的回调函数被取出并执行。在这个回调函数内部,又有一个新的Promise。这个Promiseconsole.log("promise1")是同步任务,会立即执行。而它的then方法又会生成一个微任务,被添加到微任务队列中。

arduino 复制代码
setTimeout 1
promise1

第四步:再次处理微任务队列

setTimeout的回调函数执行完毕,执行栈再次空了。事件循环会再次检查微任务队列。此时,之前在setTimeout回调中产生的微任务Promise then 1正在队列中等待。所以,它会被立即执行:

javascript 复制代码
Promise then 1

最终输出顺序:

javascript 复制代码
首次同步任务开始
首次同步任务结束
promise2
Promise then 2
setTimeout 1
promise1
Promise then 1

是不是感觉像看了一场精彩的舞蹈?同步任务是开场舞,微任务是插曲,宏任务是主旋律,而事件循环就是那个掌控全场的指挥家!

划重点:

  • 异步任务分类: 宏任务 (setTimeout, setInterval, I/O, UI Rendering),微任务 (Promise.then().catch().finally(), MutationObserver, process.nextTick)
  • 所有同步任务都在主线程上执行,形成一个执行栈。
  • 遇到异步任务,它们会被丢到任务表中,等事件执行完成(比如ajax请求完成、setTimeout设置时间到期),之后放入相应的任务队列。
  • 当执行栈的同步任务执行完成之后,事件循环会先清空微任务队列,然后从宏任务队列中取出一个任务执行。每次宏任务执行完毕,都会再次检查并清空微任务队列,然后才进行下一次循环。

⚠️ 总结与思考

好了,各位未来的前端大神们,今天我们一起深入探讨了JavaScript事件循环的奥秘。你现在应该明白了:

  1. JavaScript是单线程的,但它通过事件循环实现了非阻塞。 就像餐厅的主厨,一个人也能把所有事情安排得井井有条。
  2. 事件循环是JavaScript实现异步的关键机制。 它协调着同步任务、宏任务和微任务的执行顺序。
  3. 微任务拥有更高的优先级。 在每个事件循环周期中,微任务队列会在宏任务队列之前被完全清空。

在面试中,当面试官问到事件循环时,你不仅要能说出宏任务和微任务的分类,更要能结合代码示例,清晰地解释它们的执行顺序和背后的原理。如果你还能用一些生动形象的比喻(比如我们今天的"餐厅模型"),那绝对能让面试官眼前一亮,觉得你不仅懂技术,还能把复杂的问题讲明白!

希望这篇博客能帮助你更好地理解JavaScript事件循环,并在面试中脱颖而出!如果你有任何疑问或者想分享你的理解,欢迎在评论区留言,我们一起交流学习!

相关推荐
崔庆才丨静觅4 分钟前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60611 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了1 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅1 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅1 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment2 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅2 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊2 小时前
jwt介绍
前端
爱敲代码的小鱼2 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax