前言
关于这个事件循环,最初在慧策笔试的时候碰到过,美团一面的时候也被拷打过,面试官建议我再多去了解一下,所以写这篇文章加深一下对事件循环的理解
事件循环概念
事件循环(Event Loop)是 JavaScript 运行时的核心机制,用于处理异步操作、用户交互等事件,保证代码按顺序执行。 JavaScript 是单线程语言,意味着同一时间只能执行一个任务。为了处理异步操作(如网络请求、定时器等), JavaScript 引入了事件循环机制。事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。
工作原理
JavaScript 运行时主要包含以下几个部分:
- 调用栈(Call Stack):用于执行同步代码,遵循后进先出(LIFO)原则。当函数被调用时,会在调用栈中创建一个新的栈帧,函数执行完毕后,栈帧出栈。
- 任务队列(Task Queue):存放异步操作完成后待执行的回调函数。任务队列又分为宏任务队列(MacroTask Queue)和微任务队列(MicroTask Queue)。
- 事件循环(Event Loop):持续检查调用栈是否为空,若为空,则从任务队列中取出任务放入调用栈执行。
事件循环的工作流程如下:
- 检查调用栈是否为空,如果不为空,执行调用栈中的任务。
- 若调用栈为空,检查微任务队列是否有任务,若有则依次执行,直到微任务队列为空。
- 微任务队列清空后,从宏任务队列中取出一个任务放入调用栈执行,执行完成后,重复步骤 2 和 3。
宏任务与微任务
- 宏任务(MacroTask):常见的宏任务有 setTimeout、setInterval、setImmediate(Node.js)、requestAnimationFrame(浏览器)、I/O 操作、UI 渲染等。
- 微任务(MicroTask):常见的微任务有 Promise.then、async/await(本质上是基于 Promise)、MutationObserver(浏览器)、process.nextTick(Node.js)等。
输出代码题练习
最简单的:
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(() => {
console.log('promise1');
})
.then(() => {
console.log('promise2');
});
console.log('script end');点击查看输出顺序
script start
script end
promise1
promise2
setTimeout
执行过程:
- 执行全局代码,将
console.log('script start')压入调用栈,执行完毕后出栈。 - 遇到
setTimeout定时器,将其回调函数压入宏任务队列。 - 遇到
Promise.resolve(),将其回调函数压入微任务队列。 - 执行
console.log('script end'),将其压入调用栈,执行完毕后出栈。 - 调用栈为空,检查微任务队列,发现有
Promise.then回调函数,将其压入调用栈,执行完毕后出栈。 - 再次检查微任务队列,发现还有
Promise.then回调函数,将其压入调用栈,执行完毕后出栈。 - 微任务队列清空,从宏任务队列中取出
setTimeout回调函数,将其压入调用栈,执行完毕后出栈。 - 调用栈为空,事件循环结束。
async function async1() {
console.log('async1 start');
await new Promise(resolve => {
console.log('promise1');
setTimeout(() => {
resolve();
console.log('setTimeout1');
}, 0);
});
console.log('async1 end');
}
async function async2() {
console.log('async2 start');
await new Promise(resolve => {
setTimeout(() => {
resolve();
console.log('setTimeout2');
}, 0);
});
console.log('async2 end');
}
console.log('script start');
async1();
async2();
new Promise(resolve => {
console.log('promise2');
resolve();
}).then(() => {
console.log('promise3');
});
console.log('script end');点击查看输出顺序
script start
async1 start
promise1
async1 end
setTimeout1
async2 start
async2 end
setTimeout2
promise2
script end
promise3
执行过程:
- 执行全局代码,将
console.log('script start')压入调用栈,执行完毕后出栈。 - 遇到
async1函数,将其压入调用栈,执行 async1 函数内部代码。 - 执行
console.log('async1 start'),将其压入调用栈,执行完毕后出栈。 - 遇到
await关键字,将await后面的Promise压入微任务队列,将async1函数的剩余代码压入调用栈。 - 执行
console.log('promise1'),将其压入调用栈,执行完毕后出栈。 - 遇到
setTimeout定时器,将其回调函数压入宏任务队列。 - 执行
console.log('async1 end'),将其压入调用栈,执行完毕后出栈。 - 调用栈为空,检查微任务队列,发现有
Promise.then回调函数,将其压入调用栈,执行完毕后出栈。 - 再次检查微任务队列,为空,从宏任务队列中取出
setTimeout回调函数,将其压入调用栈,执行完毕后出栈。 - 调用栈为空,事件循环结束。
如果没有 await 关键字的话,输出顺序如下
点击查看输出顺序
script start
async1 start
promise1
async1 end
async2 start
async2 end
promise2
script end
promise3
setTimeout1
setTimeout2
await 的作用: 由于await会暂停当前async函数,将后续的代码包装成微任务,只有当promise改变状态(promise完成后),才会恢复执行
console.log("🚀 开始");
Promise.resolve()
.then(() => console.log("1️⃣ 第一个.then"))
.then(() => console.log("2️⃣ 第二个.then"))
Promise.resolve().then(() => console.log("3️⃣ 外部.then"))点击查看输出顺序
🚀 开始
1️⃣ 第一个.then
3️⃣ 外部.then
2️⃣ 第二个.then这题是我之前做错的,我当时误以为第二个then会在外部then之前执行,但是实际上是在外部then之后执行的。正确理解是每个.then只有在前一个Promise解决后才会注册
相关资料
https://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://segmentfault.com/a/1190000046362554
https://zhuanlan.zhihu.com/p/33058983
https://juejin.cn/post/6844904079353708557?searchId=20250815220628C3C5642C55BE65D2B440#heading-0

