深入浅出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) 的关键所在了!
- 主厨优先处理点餐板上的所有主菜(执行栈中的所有同步任务)。他会一道接一道地做,直到点餐板空了为止。
- 点餐板空了之后,主厨会先去微任务队列看看有没有"小吃"。如果微任务队列里有,他会把所有的小吃都取出来,一口气全部处理完。因为这些都是"加急件",必须优先处理。
- 微任务队列清空后,主厨才会去宏任务队列取一道"大菜"来处理 。注意,每次循环只会从宏任务队列中取一个任务来执行。
- 处理完这道"大菜"后,主厨会再次回到第2步,检查微任务队列。因为在处理"大菜"的过程中,可能会产生新的"小吃"。
- 如此循环往复,直到两个队列都清空,主厨才能休息。
这个过程,就是JavaScript事件循环的完整机制。它保证了JavaScript在单线程环境下,既能高效处理同步任务,又能异步处理耗时任务,并且对不同类型的异步任务进行了优先级管理。
JS引擎
- 执行栈(Call Stack):所有同步任务都在主线程上执行,形成一个执行栈。当函数被调用时,会被推入栈中;当函数执行完毕,会被弹出栈。
- 任务队列(Task Queue) :用于存放异步任务的回调函数。分为宏任务队列和微任务队列。
- 宏任务(MacroTask) :
setTimeout
,setInterval
, I/O, UI Rendering等。 - 微任务(MicroTask) :
Promise.then().catch().finally()
,MutationObserver
,process.nextTick
(Node.js环境)等。
- 宏任务(MacroTask) :
理解了这个"餐厅模型",你是不是对事件循环有了更直观的认识呢?接下来,我们通过一个实际的代码例子,来看看这个机制是如何运作的。
🔧 代码实战:宏任务与微任务的舞蹈
理论知识讲了一大堆,是不是有点晕?别急,我们来点实际的!下面这段代码,就是面试官最喜欢拿来"考考你"的经典案例。我们用它来模拟一下宏任务和微任务在事件循环中的"舞蹈"过程。
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
的同时,setTimeout
和Promise
这两个异步任务也被"登记"了。setTimeout
被扔进了宏任务队列,而Promise
的then
方法(注意,是then
方法里的回调,而不是Promise
本身)被扔进了微任务队列。
第二步:处理微任务队列
同步任务执行完毕,执行栈空了。这时候,主厨会先去微任务队列看看有没有"小吃"。嘿,果然有!promise2
和Promise then 2
正在那里等着呢。所以,它们紧接着被执行:
javascript
promise2
Promise then 2
第三步:处理宏任务队列
微任务队列清空了,主厨终于可以去宏任务队列里取"大菜"了。此时,setTimeout
的回调函数被取出并执行。在这个回调函数内部,又有一个新的Promise
。这个Promise
的console.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事件循环的奥秘。你现在应该明白了:
- JavaScript是单线程的,但它通过事件循环实现了非阻塞。 就像餐厅的主厨,一个人也能把所有事情安排得井井有条。
- 事件循环是JavaScript实现异步的关键机制。 它协调着同步任务、宏任务和微任务的执行顺序。
- 微任务拥有更高的优先级。 在每个事件循环周期中,微任务队列会在宏任务队列之前被完全清空。
在面试中,当面试官问到事件循环时,你不仅要能说出宏任务和微任务的分类,更要能结合代码示例,清晰地解释它们的执行顺序和背后的原理。如果你还能用一些生动形象的比喻(比如我们今天的"餐厅模型"),那绝对能让面试官眼前一亮,觉得你不仅懂技术,还能把复杂的问题讲明白!
希望这篇博客能帮助你更好地理解JavaScript事件循环,并在面试中脱颖而出!如果你有任何疑问或者想分享你的理解,欢迎在评论区留言,我们一起交流学习!