当前位置: 首页 > news >正文

【前端基础】事件循环 详解

文章目录

    • 一、JavaScript 的单线程特性
    • 二、事件循环的核心组成
    • 三、事件循环的运作流程
    • 四、宏任务 (Macrotask) vs 微任务 (Microtask)
    • 五、代码示例解析
    • 六、浏览器环境中的渲染时机
    • 七、Node.js 环境中的事件循环
    • 八、为什么理解事件循环很重要?

事件循环是 JavaScript 实现异步非阻塞 I/O 的核心机制,也是理解 JavaScript 代码执行顺序,特别是涉及 setTimeout, Promise, async/await 等异步操作时至关重要的概念。
在这里插入图片描述

一、JavaScript 的单线程特性

首先,我们需要明确 JavaScript 是一门单线程语言。这意味着在任意特定时刻,JavaScript 引擎只能执行一个任务。所有任务都需要排队,一个接一个地处理。

优点:

  • 避免了多线程环境下复杂的并发控制问题(如竞态条件、死锁等)。
  • 实现简单。

缺点:

  • 如果一个任务执行时间过长(例如,复杂的计算或长时间的网络请求),后续的所有任务都必须等待,导致程序阻塞,用户界面卡顿甚至无响应。这就是所谓的“阻塞 (blocking)”。

为了解决单线程带来的阻塞问题,同时又能处理耗时的操作(如 I/O 操作、定时器、用户交互等),JavaScript 引入了异步编程模型和事件循环机制。

二、事件循环的核心组成

事件循环模型主要由以下几个关键部分组成,尤其是在浏览器环境中:

  1. 调用栈 (Call Stack / Execution Stack)

    • 一个后进先出 (LIFO) 的数据结构,用于存储所有正在执行的函数调用的上下文。
    • 当一个函数被调用时,它的帧 (frame) 会被推入调用栈。
    • 当函数执行完毕返回时,它的帧会从调用栈中弹出。
    • 所有同步代码都在调用栈中执行。
  2. 堆 (Heap)

    • 一块内存区域,用于存储对象、数组等复杂数据类型。与事件循环的直接关系不大,但与 JavaScript 的内存管理相关。
  3. 宿主环境提供的 API (Web APIs / Node.js APIs)

    • 这些 API 由 JavaScript 的宿主环境(浏览器或 Node.js)提供,它们不是 JavaScript 引擎的一部分。
    • 例如:
      • 浏览器环境DOM API (如事件监听), setTimeout(), setInterval(), XMLHttpRequest, Workspace(), requestAnimationFrame() 等。
      • Node.js 环境fs (文件系统), http (网络), child_process, timers 模块等。
    • 这些 API 允许我们发起异步操作。当调用这些 API 时,操作会在宿主环境的后台线程中处理,而不会阻塞 JavaScript 主线程。
  4. 任务队列 (Task Queue / Callback Queue)
    事件循环中有两种主要的任务队列:

    • 宏任务队列 (Macrotask Queue / Task Queue)
      • 用于存放宏任务 (Macrotask) 的回调函数。
      • 常见的宏任务来源:
        • setTimeout(), setInterval() 的回调
        • setImmediate() (Node.js 环境)
        • I/O 操作的回调 (如文件读写、网络请求完成)
        • UI 交互事件的回调 (如点击、键盘输入、滚动等)
        • 脚本 (<script>) 本身的执行 (可以看作是一个初始的宏任务)
        • UI 渲染 (浏览器环境下,通常在处理完微任务后,在两次宏任务之间或特定时机进行)
    • 微任务队列 (Microtask Queue)
      • 用于存放微任务 (Microtask) 的回调函数。
      • 微任务通常具有更高的优先级,会在当前宏任务执行完毕后、下一个宏任务开始前立即执行。
      • 常见的微任务来源:
        • Promise.then(), Promise.catch(), Promise.finally() 的回调
        • async/awaitawait 关键字之后的代码 (实际上是 Promise 的封装)
        • MutationObserver 的回调
        • queueMicrotask() API
        • process.nextTick() (Node.js 环境,优先级甚至高于其他微任务)

三、事件循环的运作流程

事件循环是一个持续不断的过程,它的基本工作流程如下:

  1. 执行同步代码

    • 首先,JavaScript 引擎会执行全局的同步代码(通常是 <script> 标签中的代码,这可以看作是第一个宏任务)。
    • 所有同步函数调用都会被压入调用栈并依次执行。
  2. 遇到异步 API 调用

    • 当遇到异步 API 调用(如 setTimeout, WorkspacePromise 的创建),JavaScript 引擎会将这些操作交给相应的宿主环境 API 处理。
    • 宿主环境 API 会在后台处理这些异步任务。
    • 当异步任务完成(例如定时器到期、数据获取成功/失败、Promise 状态改变),宿主环境会将相应的回调函数放入对应的任务队列中(宏任务队列或微任务队列)。
  3. 事件循环的监控

    • 事件循环会持续不断地检查调用栈是否为空
  4. 处理微任务

    • 一旦调用栈为空(即当前宏任务中的所有同步代码执行完毕),事件循环会立即检查微任务队列
    • 如果微任务队列不为空,事件循环会按顺序执行队列中所有的微任务,直到微任务队列变空为止。
    • 重要:如果在执行微任务的过程中,又产生了新的微任务,这些新的微任务也会被添加到微任务队列的末尾,并在当前轮次的微任务处理中被执行完毕。微任务队列必须在下一个宏任务开始前被完全清空。
  5. 处理宏任务

    • 当调用栈为空且微任务队列也为空之后,事件循环会检查宏任务队列。
    • 如果宏任务队列不为空,事件循环会取出一个最早进入队列的宏任务,将其回调函数压入调用栈执行。
    • 这个宏任务的执行过程与步骤 1 类似,它可能包含同步代码和新的异步 API 调用。
  6. 重复循环

    • 当这个新的宏任务执行完毕(调用栈再次变空),事件循环会再次执行步骤 4(检查并清空微任务队列),然后执行步骤 5(处理下一个宏任务),如此往复,形成一个持续的循环。

简单概括一轮事件循环 (Tick):
一个宏任务 -> 执行该宏任务中的所有同步代码 -> 清空所有微任务 -> (可能会进行UI渲染) -> 下一个宏任务…

四、宏任务 (Macrotask) vs 微任务 (Microtask)

理解宏任务和微任务的区别是掌握事件循环的关键:

  • 执行时机
    • 微任务在当前宏任务执行结束后、下一个宏任务开始前立即执行。
    • 宏任务则需要等待前面的宏任务以及所有微任务都执行完毕后,才会按队列顺序执行。
  • 优先级:微任务的优先级高于宏任务。
  • 队列特性
    • 通常一次事件循环只会执行一个宏任务(从宏任务队列中取一个)。
    • 但会执行所有当前可用的微任务,直到微任务队列为空。

一个形象的比喻:

  • 宏任务队列:像是银行的排队叫号系统,一次处理一个客户(宏任务)。
  • 微任务队列:像是银行的 VIP 通道或紧急业务窗口。当一个普通客户(宏任务)办理完业务后,银行会立即处理所有 VIP 通道和紧急业务窗口的客户(微任务),直到这些客户都处理完毕,才会叫下一个普通号。

五、代码示例解析

让我们通过一个经典的例子来理解事件循环:

console.log('Script start'); // 1. 同步代码setTimeout(function() { // 注册宏任务 M1console.log('setTimeout callback'); // M1.1
}, 0);Promise.resolve().then(function() { // 注册微任务 m1console.log('Promise.then callback 1'); // m1.1
}).then(function() { // 注册微任务 m2 (由 m1 返回的 Promise 产生)console.log('Promise.then callback 2'); // m2.1
});console.log('Script end'); // 2. 同步代码

执行顺序分析:

  1. 全局同步代码执行 (第一个宏任务的一部分)

    • console.log('Script start'); 输出: Script start
    • 遇到 setTimeout,其回调函数被注册为一个宏任务 (M1),放入宏任务队列。
    • Promise.resolve() 创建一个已解决的 Promise,其第一个 .then() 的回调函数被注册为一个微任务 (m1),放入微任务队列。
    • console.log('Script end'); 输出: Script end
    • 调用栈变空。
  2. 处理微任务队列

    • 事件循环检查微任务队列,发现不为空。
    • 执行微任务 m1: console.log('Promise.then callback 1'); 输出: Promise.then callback 1
    • m1 的 .then() 返回一个新的 Promise,其回调函数(打印 “Promise.then callback 2”)被注册为新的微任务 (m2),放入微任务队列末尾。
    • 微任务队列现在是 [m2]。事件循环继续处理微任务。
    • 执行微任务 m2: console.log('Promise.then callback 2'); 输出: Promise.then callback 2
    • 微任务队列变空。
  3. 处理宏任务队列 (下一轮事件循环)

    • 事件循环检查宏任务队列,发现 M1。
    • 取出 M1,将其回调函数压入调用栈执行。
    • 执行 M1 的回调: console.log('setTimeout callback'); 输出: setTimeout callback
    • 调用栈变空。
    • 再次检查微任务队列(此时为空)。

最终输出顺序:

Script start
Script end
Promise.then callback 1
Promise.then callback 2
setTimeout callback

六、浏览器环境中的渲染时机

在浏览器中,页面的渲染更新通常也作为事件循环的一部分。浏览器一般会尝试在每秒60帧(约16.7ms一次)的频率更新页面。

  • 渲染操作通常被认为是宏任务(或者在宏任务之间进行)。
  • 重要的是,所有微任务会在下一次渲染之前执行完毕。这意味着,如果微任务执行时间过长,或者不断有新的微任务被添加到队列中,可能会阻塞或延迟页面的渲染,导致用户感觉卡顿。
  • requestAnimationFrame() 是一个特殊的 API,它的回调会在浏览器下一次重绘之前执行,非常适合用来执行动画更新。它本身可以被看作是与渲染紧密相关的另一种类型的任务。

七、Node.js 环境中的事件循环

Node.js 的事件循环机制与浏览器类似,但也有其自身的特点和更复杂的阶段划分。Node.js 事件循环的主要阶段包括:

  1. timers (定时器):执行 setTimeout()setInterval() 的回调。
  2. pending callbacks (待定回调):执行延迟到下一个循环迭代的 I/O 回调。
  3. idle, prepare (仅内部使用)
  4. poll (轮询):检索新的 I/O 事件;执行与 I/O 相关的回调(除了关闭回调、定时器回调和 setImmediate())。Node.js 在这里可能会阻塞,等待新的连接、数据等。
  5. check (检查):执行 setImmediate() 的回调。
  6. close callbacks (关闭回调):例如 socket.on('close', ...)

在 Node.js 中,process.nextTick() 的回调并不完全属于上述阶段,它们会在当前操作完成后、事件循环继续到下一个阶段之前立即执行,优先级非常高,甚至高于其他微任务。Promise 的回调也是微任务,在每个阶段完成后、进入下一个阶段前,以及 process.nextTick() 队列清空后执行。

总的来说,Node.js 的事件循环更为复杂,但“宏任务-微任务”的基本处理逻辑(即每个宏任务阶段完成后清空微任务队列)是相似的。

八、为什么理解事件循环很重要?

  • 编写高性能代码:避免长时间运行的同步代码阻塞主线程,合理利用异步操作提升应用响应性。
  • 调试异步问题:理解代码的实际执行顺序,帮助定位和解决异步相关的 bug。
  • 掌握异步模式:更好地使用 Promise, async/await 等现代异步编程工具。
  • 避免常见陷阱:例如,过度依赖 setTimeout(fn, 0) 的执行时机,或者不理解微任务可能导致的渲染延迟。

事件循环是 JavaScript 异步编程的基石,深刻理解它能够让你更自如地驾驭 JavaScript 的异步特性,编写出更健壮、更高效的应用程序。

相关文章:

  • HarmonyOS运动开发:如何绘制运动速度轨迹
  • ET CircularBuffer 类
  • 09、供应商管理数字化转型:从潜在评估到战略合作的系统化方法
  • 2025-5-26Vue3快速上手
  • 达梦JNI方式调用Logmnr接口调用示例
  • 测试W5500的第7步_使用ioLibrary库创建HTTP客户端
  • 学习心得(14--16)
  • python打卡训练营打卡记录day37
  • day28:零基础学嵌入式之进程2
  • 轻量级视觉语言模型 Dolphin:高效精准的文档结构化解析利器
  • AI算力网络光模块市场发展分析
  • 202505系分论文《论模型驱动分析方法及应用》
  • 基于大模型的胃肠道功能紊乱手术全程预测与干预方案研究
  • 统一人体姿态估计与分割的新方法:KDC
  • 《DeepSeek行业应用全景指南(视频微课版)》:从入门到精通的AI落地实践手册
  • 身份认证: JWT和Session是什么?
  • 【Java】异常处理
  • 信息学奥赛一本通 1547:【 例 1】区间和
  • AlphaCore GPU 物理仿真引擎内测邀请
  • 高并发系统下Mutex锁、读写锁、线程重入锁的使用思考
  • 高阳网站建设/深圳做网站的
  • 信息网站建设预算/资源猫
  • 大良营销网站建设市场/变现流量推广app
  • 团购网站大全/镇江网站建设方案
  • 网站官方认证怎么做/茂名网站建设制作
  • 天河商城型网站建设/网站怎么添加外链