核心概念和组件
JavaScript是单线程的
- 这意味着它一次只能执行一段代码。
- 如果所有操作(网络请求、文件读取、定时器、用户点击)都同步执行,那么耗时的操作会阻塞整个页面,导致页面无响应。事件循环就是为了解决这个问题而存在的。
关键组件
- 调用栈
Call Stack
- 一个后进先出
LIFO
的数据结构.- 负责跟踪当前正在执行的函数。
- 每调用一个函数,就将其压入栈顶;函数执行完毕
return
,就将其弹出栈。- 同步代码在这里按顺序执行。
- 堆
Heap
一个非结构化的内存区域,用于存储对象(变量、函数等分配的内存);
- 任务队列
Callback Queue
/Task Queue
/MacroTask Queue
- 一个先进先出
FIFO
的数据结构。- 存放宏任务
MacroTask
的回调函数。- 常见的宏任务来源:
setTimeout
/setInterval
/setImmediate(Node.js)
/I/O操作
(如网络请求完成、文件读取完成)/UI渲染(浏览器)
/DOM事件(click
/load
)的回调。
- 微任务队列
Microtask Queue
/Job Queue
- 一个先进先出
FIFO
的数据结构,但优先级高于任务队列。- 存放微任务的回调函数。
- 常见的微任务来源:
Promise.then
/Promise.catch
/Promise.finally
/MutationObserver
(浏览器)/queueMicrotask
- 事件循环
Event Loop
- 一个持续运行的进程,负责协调调用栈、任务队列和微任务队列。
- 它的核心职责是:当调用栈为空时,检查队列并安排下一个任务执行。
工作流程
事件循环遵循一个非常具体的循环过程:
执行同步代码Initial Execution
- 脚本开始执行时,所有的同步代码被依次压入调用栈执行。
- 这是事件循环的第一个轮回的开始。
执行当前调用栈
- 事件循环首先处理调用栈中的任务,直到调用栈完全清空。
执行所有微任务Process Microtasks
- 一旦调用栈为空 ,事件循环会立即检查微任务队列。
- 它会连续不断地、一次性执行完微任务队列中所有已存在的微任务回调函数(直到微任务队列为空)。
- 在执行一个微任务的过程中,该微任务可能又会产生新的微任务(例如:在
Promise.then()
中又返回一个新的Priomise并调用其.then()
)。这些新产生的微任务会被添加到微任务队列的末尾,并在当前这轮微任务处理循环中被立即执行 ,直到队列真正清空。这是微任务处理的关键特点:一个宏任务之后,会清空整个微任务队列(包括期间新产生的微任务)。
是否需要渲染(Update Rendering
-浏览器特有)
- 「此步骤主要针对浏览器环境」 在微任务队列清空后,浏览器可能会执行渲染更新(布局
Layout
/绘制Paint
)。但这不是事件循环规范的一部分,而是浏览器实现时的优化点。渲染发生的时机由浏览器决定,通常尝试与屏幕刷新率同步(如每秒60次)。
执行一个宏任务Run a MacroTask
- 微任务队列清空(/渲染后),事件循环检查 (宏)任务队列。
- 如果任务队列中有等待的任务,事件循环取出队列中最前面的一个宏任务 (最早入队的那个),将其回调函数压入调用栈执行。
- 注意:每次循环只执行一个宏任务(如果在执行这个宏任务的过程中产生了新的宏任务,新的宏任务要等到下一轮循环才执行)。
重复循环Loop
- 执行完步骤5中的一个宏任务后,调用栈再次变空。
- 事件循环 立即回到步骤「执行所有微任务」 。
- 然后按顺序执行随后步骤,这个过程无限循环下去。
关键点与注意事项
- 微任务优先:微任务队列的优先级远高于宏任务队列。每当调用栈清空(无论是初始同步代码执行完,还是一个宏任务执行完),事件循环都会先去清空整个微任务队列,然后才考虑执行下一个宏任务。
- 宏任务一次一个
- UI渲染时机 :在浏览器中,渲染通常发生在微任务队列清空之后,下一个宏任务执行之前。这意味着在微任务中进行大量的同步操作会阻塞渲染,导致页面卡顿。
- setTimeout(fn, 0)并不精确 :它表示"尽快"将
fn
的回调放入宏任务队列,但实际执行至少要等到当前调用栈和微任务队列清空之后,并且前面可能还有其它宏任务在排队。浏览器通常还有最小延迟(如4ms)。 Node.js
vs浏览器:核心的事件循环概念(宏任务/微任务)是相同的。区别在于:
- 宏任务来源 :Node.js有
setImmediate
(通常比setTimeout(fn, 0)
优先级更高)、I/O
回调、特定于Node的事件。- 微任务来源 :
Promise
回调、process.nextTick()
(netTick
队列是一个特殊的队列-Node.js特有,其优先级甚至高于微任务队列,会在当前操作结束后、事件循环继续之前立即执行)。- 阶段划分 :Node.js的事件循环被更精细地划分为多个阶段(
timers
,pending callbacks
,idle/prepare
,poll
,check
,close callbacks
),每个阶段处理特地类型的宏任务。微任务(和netTick
)在阶段切换之间执行。
- 避免阻塞 :长时间运行的同步代码或微任务(如大型循环、复杂计算)会阻塞调用栈,导致事件循环无法处理队列中的任务(宏任务/微任务)和渲染,造成页面卡死。务必使用异步操作或将耗时任务分块(
setTimeout
/requestIdleCallback
/Web Workers
)
经典案例分析
通用场景
javascript
console.log('script start'); // 1. 同步
setTimeout(() => {
console.log('setTimeout'); // 5. 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('promise 1'); // 3. 微任务
}).then(() => {
console.log('promise 2'); // 4. 微任务(在上一个微任务执行过程中产生,立即执行)
});
console.log('script end'); // 2. 同步
Node.js
环境
javascript
console.log('Start'); // 同步
setTimeout(() => console.log('setTimeout 1'), 0); // 宏任务
process.nextTick(() => {
console.log('nextTick 1');
setTimeout(() => console.log('setTimeout inside nextTick'), 0); // 微任务 timers?
});
new Promise(resolve => {
console.log('Promise executor'); // 同步
resolve();
}).then(() => {
console.log('Promise then 1'); // 微任务
process.nextTick(() => console.log('nextTick inside Promise then'));
});
async function asyncFunc() {
console.log('Async function start'); // 同步
await new Promise(resolve => resolve());
console.log('After await'); // 微任务
process.nextTick(() => console.log('nextTick after await'));
}
setImmediate(() => console.log('setImmediate')); // 宏任务
asyncFunc();
process.nextTick(() => console.log('nextTick 2'));
console.log('End'); // 同步
解析
- 执行同步代码
Start
Promise executor
Async function start
End
- 处理nextTick队列
nextTick 1 nextTick 2
执行时:
nextTick 1
输出,并添加setTimeout到timers队列nextTick 2
输出
- 处理微任务队列(
Promise
回调)
Promise then 1 After await
执行时:
Promise then 1
输出- 添加
() => console.log('nextTick inside Promise then')
到nextTick队列 - 处理
await
隐式Promise
:asyncFunc
中的await
产生一个微任务 - 添加
() => console.log('nextTick after await')
到nextTick队列
- 再次处理
nextTick
队列
nextTick inside Promise then nextTick after await
- 进入事件循环(
timers
阶段)
setTimeout 1
setTimeout inside nextTick
- 进入
check
阶段(setImmediate
)
setImmediate
关键点
- 执行顺序优先级 :同步代码 ->
process.nextTick()
-> 微任务 -> 宏任务 process.nextTick()
特性 :- 在当前操作结束后立即执行
- 优先级高于微任务队列
- 递归调用会导致事件循环
饿死
async
/await
本质 :await
之后的代码相当于放在Promise.then()
中asyncFunc
的调用同步执行直到第一个await
- 事件循环阶段 :
timers
阶段执行setTimeout
/setInterval
check
阶段执行setImmediate
- 微任务执行时机 :
- 在每个阶段切换之间执行
- 在
nextTick
队列清空后执行