事件循环机制的具体流程可以拆解为 “主线程执行栈”“任务队列”“循环调度” 三个核心环节的协作,以下是详细步骤:
一、初始化:区分任务类型
- 同步任务:进入主线程的 “执行栈”,按顺序立即执行(如变量声明、函数调用、条件判断等)。
- 异步任务:不进入执行栈,而是被 “宿主环境”(浏览器 / Node.js)接管,等待触发条件(如定时器到期、网络请求完成、DOM 事件触发)。
触发后,异步任务的回调函数会被放入对应的任务队列(分为宏任务队列和微任务队列)。
二、核心循环流程
事件循环按固定规则重复以下步骤,直到所有任务执行完毕:
步骤 1:清空执行栈
- 主线程优先执行执行栈中的所有同步任务,直到执行栈为空(即当前同步代码全部执行完毕)。
步骤 2:处理微任务队列
- 检查微任务队列,如果有任务:
- 按顺序将微任务从队列中取出,放入执行栈执行,直到微任务队列清空(即使执行过程中新增了微任务,也会在本轮一并执行)。
- 微任务类型包括:
Promise.then/catch/finally
、async/await
(本质是 Promise 的语法糖)、queueMicrotask()
、Node.js 中的process.nextTick
(优先级高于其他微任务)。
步骤 3:处理 UI 渲染(浏览器环境特有)
- 微任务队列清空后,浏览器会触发一次 UI 渲染(如 DOM 更新、样式计算等),Node.js 环境无此步骤。
步骤 4:处理宏任务队列
- 从宏任务队列中取出一个任务(按入队顺序,先进先出),放入执行栈执行。
- 执行完毕后,该宏任务的本轮处理结束。
步骤 5:重复循环
- 回到步骤 1,再次检查执行栈是否为空,重复上述流程(清空执行栈→处理微任务→UI 渲染→处理一个宏任务)。
三、关键规则总结
- 微任务优先于宏任务:每处理完一个宏任务后,必须先清空所有微任务,再处理下一个宏任务。
- 宏任务一次处理一个:即使宏任务队列中有多个任务,每次循环只取一个执行,避免长时间阻塞主线程。
- 微任务即时处理:微任务队列中的任务会被一次性全部执行(包括执行过程中新增的微任务),确保异步操作的 “即时性”(如 Promise 的回调需要尽快执行)。
四、示例:直观理解流程
以下代码的执行顺序可清晰体现事件循环流程:
javascript
// 同步任务1
console.log('同步任务1');
// 宏任务:setTimeout
setTimeout(() => {
console.log('宏任务1(setTimeout)');
// 宏任务中新增的微任务
Promise.resolve().then(() => console.log('宏任务1中的微任务'));
}, 0);
// 微任务:Promise.then
Promise.resolve().then(() => {
console.log('微任务1(Promise)');
// 微任务中新增的微任务
Promise.resolve().then(() => console.log('微任务2(嵌套)'));
// 微任务中新增的宏任务
setTimeout(() => console.log('微任务中新增的宏任务'), 0);
});
// 同步任务2
console.log('同步任务2');
执行顺序解析:
- 执行同步任务:
同步任务1
→同步任务2
(执行栈清空)。 - 处理微任务队列:
- 执行第一个微任务:
微任务1(Promise)
。 - 执行过程中新增的微任务入队,继续执行:
微任务2(嵌套)
(微任务队列清空)。
- 执行第一个微任务:
- 浏览器 UI 渲染(假设此时有 DOM 更新)。
- 处理宏任务队列中的第一个任务:
宏任务1(setTimeout)
。- 执行过程中新增的微任务入队,执行栈清空后,处理该微任务:
宏任务1中的微任务
。
- 执行过程中新增的微任务入队,执行栈清空后,处理该微任务:
- 再次循环,处理宏任务队列中新增的任务:
微任务中新增的宏任务
。
最终输出顺序:
plaintext
同步任务1
同步任务2
微任务1(Promise)
微任务2(嵌套)
宏任务1(setTimeout)
宏任务1中的微任务
微任务中新增的宏任务
总结
事件循环的核心逻辑是:“同步优先,微任务次之,宏任务最后,循环往复”。通过这种机制,单线程的 JavaScript 既能保证同步任务的有序执行,又能高效处理异步操作,避免因等待而阻塞,实现 “非阻塞” 的并发效果。
在事件循环(Event Loop)过程中,对于执行微任务或宏任务时新进来的同步任务、微任务的处理方式如下:
执行微任务时新任务的处理
1. 同步任务:如果在执行微任务的过程中遇到同步任务,同步任务会立即在当前调用栈中执行。因为JavaScript是单线程的,调用栈在同一时间只能执行一个任务,所以新的同步任务会打断微任务的执行流程,优先执行完同步任务后,才会继续执行微任务剩余部分。例如:
Promise.resolve().then(() => {
console.log('微任务开始');
// 同步任务
for (let i = 0; i < 1000; i++) {
console.log(i);
}
console.log('微任务结束');
});
在这个例子中, for 循环这个同步任务会在微任务的回调函数中立即执行,执行完后才会输出 微任务结束 。
2. 微任务:在执行微任务过程中产生的新微任务,会被添加到微任务队列的末尾。当当前微任务执行完毕(包括执行过程中遇到的同步任务执行完毕),事件循环会继续从微任务队列中取出下一个微任务执行,直到微任务队列为空。例如:
Promise.resolve().then(() => {
console.log('第一个微任务开始');
Promise.resolve().then(() => {
console.log('新产生的微任务');
});
console.log('第一个微任务结束');
});
输出顺序为: 第一个微任务开始 , 第一个微任务结束 , 新产生的微任务 。因为新产生的微任务被添加到微任务队列末尾,在第一个微任务执行完毕后才会执行。
执行宏任务时新任务的处理
1. 同步任务:在执行宏任务过程中遇到同步任务,同样会立即在当前调用栈中执行。同步任务会按照代码顺序依次执行,直到宏任务中的同步代码执行完毕。例如:
setTimeout(() => {
console.log('宏任务开始');
// 同步任务
for (let i = 0; i < 1000; i++) {
console.log(i);
}
console.log('宏任务结束');
}, 0);
这里的 for 循环同步任务会在 setTimeout 的回调函数(宏任务)中立即执行,执行完后输出 宏任务结束 。
2. 微任务:宏任务执行过程中产生的微任务,会被添加到微任务队列的末尾。当宏任务执行完毕(包括宏任务中的同步任务执行完毕),调用栈为空时,事件循环会开始处理微任务队列,依次执行微任务队列中的所有微任务,直到微任务队列为空,然后才会从宏任务队列中取出下一个宏任务执行。例如:
setTimeout(() => {
console.log('宏任务开始');
Promise.resolve().then(() => {
console.log('宏任务中产生的微任务');
});
console.log('宏任务结束');
}, 0);
输出顺序为: 宏任务开始 , 宏任务结束 , 宏任务中产生的微任务 。因为宏任务中产生的微任务在宏任务执行完毕后,才会在处理微任务队列时被执行。
综上所述,在事件循环中,无论是宏任务还是微任务执行过程中产生的新任务,都会遵循相应规则进行处理,确保JavaScript单线程环境下任务的有序执行。