事件循环机制

核心概念和组件

JavaScript是单线程的

  • 这意味着它一次只能执行一段代码。
  • 如果所有操作(网络请求、文件读取、定时器、用户点击)都同步执行,那么耗时的操作会阻塞整个页面,导致页面无响应。事件循环就是为了解决这个问题而存在的。

关键组件

  • 调用栈Call Stack
  1. 一个后进先出LIFO的数据结构.
  2. 负责跟踪当前正在执行的函数。
  3. 每调用一个函数,就将其压入栈顶;函数执行完毕return,就将其弹出栈。
  4. 同步代码在这里按顺序执行。
  • Heap

一个非结构化的内存区域,用于存储对象(变量、函数等分配的内存);

  • 任务队列Callback Queue/Task Queue/MacroTask Queue
  1. 一个先进先出FIFO的数据结构。
  2. 存放宏任务MacroTask 的回调函数。
  3. 常见的宏任务来源:setTimeout/setInterval/setImmediate(Node.js)/I/O操作(如网络请求完成、文件读取完成)/UI渲染(浏览器)/DOM事件(click/load)的回调。
  • 微任务队列Microtask Queue/Job Queue
  1. 一个先进先出FIFO的数据结构,但优先级高于任务队列
  2. 存放微任务的回调函数。
  3. 常见的微任务来源:Promise.then/Promise.catch/Promise.finally/MutationObserver(浏览器)/queueMicrotask
  • 事件循环Event Loop
  1. 一个持续运行的进程,负责协调调用栈、任务队列和微任务队列。
  2. 它的核心职责是:当调用栈为空时,检查队列并安排下一个任务执行

工作流程

事件循环遵循一个非常具体的循环过程:

执行同步代码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.jsvs浏览器:核心的事件循环概念(宏任务/微任务)是相同的。区别在于:
  1. 宏任务来源 :Node.js有setImmediate(通常比setTimeout(fn, 0)优先级更高)、I/O回调、特定于Node的事件。
  2. 微任务来源Promise回调、process.nextTick()(netTick队列是一个特殊的队列-Node.js特有,其优先级甚至高于微任务队列,会在当前操作结束后、事件循环继续之前立即执行)。
  3. 阶段划分 :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');  // 同步

解析

  1. 执行同步代码

Start

Promise executor

Async function start

End

  1. 处理nextTick队列

nextTick 1 nextTick 2

执行时:

  • nextTick 1输出,并添加setTimeout到timers队列
  • nextTick 2输出
  1. 处理微任务队列(Promise回调)

Promise then 1 After await

执行时:

  • Promise then 1输出
  • 添加() => console.log('nextTick inside Promise then')到nextTick队列
  • 处理await隐式PromiseasyncFunc中的await产生一个微任务
  • 添加() => console.log('nextTick after await')到nextTick队列
  1. 再次处理nextTick队列

nextTick inside Promise then nextTick after await

  1. 进入事件循环(timers阶段)

setTimeout 1

setTimeout inside nextTick

  1. 进入check阶段(setImmediate

setImmediate

关键点

  1. 执行顺序优先级 :同步代码 -> process.nextTick() -> 微任务 -> 宏任务
  2. process.nextTick()特性
    • 在当前操作结束后立即执行
    • 优先级高于微任务队列
    • 递归调用会导致事件循环饿死
  3. async/await本质
    • await之后的代码相当于放在Promise.then()
    • asyncFunc的调用同步执行直到第一个await
  4. 事件循环阶段 :
    • timers阶段执行setTimeout/setInterval
    • check阶段执行setImmediate
  5. 微任务执行时机
    • 在每个阶段切换之间执行
    • nextTick队列清空后执行
相关推荐
爷_35 分钟前
Nest.js 最佳实践:异步上下文(Context)实现自动填充
前端·javascript·后端
爱上妖精的尾巴1 小时前
3-19 WPS JS宏调用工作表函数(JS 宏与工作表函数双剑合壁)学习笔记
服务器·前端·javascript·wps·js宏·jsa
草履虫建模1 小时前
Web开发全栈流程 - Spring boot +Vue 前后端分离
java·前端·vue.js·spring boot·阿里云·elementui·mybatis
—Qeyser1 小时前
让 Deepseek 写电器电费计算器(html版本)
前端·javascript·css·html·deepseek
UI设计和前端开发从业者2 小时前
从UI前端到数字孪生:构建数据驱动的智能生态系统
前端·ui
Junerver2 小时前
Kotlin 2.1.0的新改进带来哪些改变
前端·kotlin
千百元3 小时前
jenkins打包问题jar问题
前端
喝拿铁写前端3 小时前
前端批量校验还能这么写?函数式校验器组合太香了!
前端·javascript·架构
巴巴_羊3 小时前
6-16阿里前端面试记录
前端·面试·职场和发展
我是若尘3 小时前
前端遇到接口批量异常导致 Toast 弹窗轰炸该如何处理?
前端