彻底理解事件循环(Event Loop):从单线程到异步世界的桥梁

news2025/5/20 17:38:04

关于事件循环被问了很多次,也遇到过很多次,一直没有系统整理,网上搜的,基本明白但总感觉不够透彻,最后,自己动手,丰衣足食,哈哈

一、为什么需要事件循环?—— 单线程的困境

JavaScript 作为浏览器的核心脚本语言,从诞生起就被设计为单线程执行模式。这意味着同一时间只能处理一个任务,比如执行一段计算代码、响应一次点击事件、解析一段 JSON 数据。

这种设计带来了两个核心问题:

  1. 阻塞风险:如果某个任务耗时过长(如复杂计算),会导致整个程序卡住

  2. 异步需求:网络请求、定时器、用户输入等异步操作无法直接同步处理

为了解决这些问题,JavaScript 引入了事件循环(Event Loop)机制,它就像一个后台管家,负责协调异步任务的执行顺序,让单线程环境也能高效处理复杂的异步场景。

二、事件循环的核心组成部分

要理解事件循环,需要先掌握三个关键概念(以浏览器环境为例):

1. 调用栈(Call Stack)

  • 作用:记录当前正在执行的函数调用栈

  • 特点:遵循 "后进先出" 原则,主线程直接操作调用栈

  • 示例:当执行a(); b();时,调用栈会先压入 a 函数,执行完弹出,再压入 b 函数

2. 任务队列(Task Queue)

  • 作用:存放异步操作完成后的回调任务(宏任务)

  • 常见类型
  • setTimeout/setInterval定时器回调

  • 原生 Promise 的then/catch/finally(注意:Promise 构造函数是同步执行!)

  • I/O 操作(如 Fetch 网络请求回调)

  • UI 渲染任务

3. 微任务队列(Microtask Queue)

  • 作用:存放需要尽快执行的高优先级异步任务

  • 关键特性
    • 优先级高于任务队列中的宏任务
    • 每次事件循环开始前会先清空微任务队列
  • 常见类型
    • Promise.then()回调(注意:async/await本质也是 Promise)
    • MutationObserver(DOM 变化监听)
    • Node.js 中的process.nextTick(优先级更高)

三、事件循环的执行流程 —— 阶段解析图

**

(图示说明:箭头表示执行顺序,队列存放对应任务)

核心执行步骤:

  1. 初始化阶段:创建调用栈、任务队列、微任务队列
  1. 执行同步代码:主线程先执行全局作用域中的同步代码
  1. 处理微任务
    • 按加入顺序执行微任务队列中的所有任务
    • 每次执行微任务时可能会向队列中添加新的微任务(递归处理)
  1. 执行宏任务
    • 从任务队列中取出一个最旧的宏任务(先进先出)
    • 将其回调放入调用栈执行
  1. 渲染阶段(浏览器环境特有):
    • 检查是否有 UI 更新任务(如requestAnimationFrame)
    • 合并多次 DOM 操作,执行最终渲染
  1. 循环往复:重复步骤 3-5,直到所有队列清空

关键规则:

  • 微任务优先:每次事件循环开始前必先处理完所有微任务
  • 宏任务排队:同一类型的宏任务按创建顺序执行
  • 阶段切换:浏览器和 Node.js 的具体阶段划分略有不同(后文详述)

四、宏任务 vs 微任务:用代码看差异

 

console.log('同步代码1');

setTimeout(() => {

console.log('定时器回调(宏任务)');

}, 0);

Promise.resolve()

.then(() => {

console.log('Promise微任务1');

return Promise.resolve(); // 新增微任务

})

.then(() => {

console.log('Promise微任务2');

});

console.log('同步代码2');

执行顺序解析:

  1. 同步代码执行:输出 "同步代码 1" → "同步代码 2"
  1. 微任务队列:两个then回调加入队列
  1. 处理微任务:
    • 执行第一个then:输出 "微任务 1",新增第二个then到队列
    • 执行第二个then:输出 "微任务 2"
  1. 处理宏任务:定时器回调执行,输出 "定时器回调"

核心结论:微任务会在当前同步代码执行完毕后,立即在同一事件循环中执行,而宏任务需要等待下一次事件循环。

五、浏览器 vs Node.js:事件循环的差异

虽然核心机制相同,但具体阶段划分存在差异:

浏览器环境(重点阶段):

  1. Timer 阶段:处理setTimeout/setInterval回调
  1. I/O 阶段:处理网络请求、文件操作等 I/O 回调
  1. Idle/Prepare 阶段:Node.js 特有,浏览器无此阶段
  1. Poll 阶段:处理 I/O 完成后的回调,控制事件循环节奏
  1. Check 阶段:处理setImmediate(Node.js)或requestIdleCallback(浏览器)
  1. Close 阶段:处理close事件回调(如 TCP 连接关闭)

Node.js 环境(v11 + 阶段划分):

 

┌───────────────────┐

│ timers │ (处理setTimeout/setInterval)

├────────────┬──────────┤

│ I/O callbacks │ (处理网络、文件I/O回调)

├────────────┼──────────┤

│ idle/prepare │ (内部使用,用户不可操作)

├────────────┼──────────┤

│ poll │ (等待I/O事件,处理新的I/O回调)

├────────────┼──────────┤

│ check │ (处理setImmediate回调)

└────────────┴──────────┘

close callbacks (处理close事件)

关键差异

  • Node.js 的process.nextTick微任务优先级高于所有阶段
  • 浏览器的事件循环与 UI 渲染强绑定(如每秒 60 次的requestAnimationFrame)

六、实战应用:如何利用事件循环优化代码

1. 避免长时间阻塞主线程

 

// 错误示例:同步计算阻塞主线程

function heavyCalculation() {

for(let i=0; i<10086; i++) {} // 耗时操作

}

// 正确做法:分解任务到宏任务队列

function splitTask() {

setTimeout(heavyCalculation, 0); // 下一次事件循环执行

// 或使用requestIdleCallback(空闲时执行)

}

2. 理解异步代码执行顺序

 

async function asyncDemo() {

console.log('async函数开始');

await Promise.resolve(); // 相当于then回调

console.log('await之后');

}

console.log('同步代码');

asyncDemo();

console.log('async调用结束');

// 输出顺序:

// 同步代码 → async函数开始 → async调用结束 → await之后(微任务阶段)

3. 处理海量数据时的性能优化

  • 使用setImmediate(Node.js)或setTimeout(0ms)拆分批量操作
  • 利用微任务处理高优先级回调(如状态更新后的立即通知)

七、总结:事件循环的本质与价值

事件循环的本质是单线程环境下的异步协调机制,它通过以下方式解决异步难题:

  1. 任务分类:通过宏任务 / 微任务队列实现优先级管理
  1. 循环处理:通过不断往返于 "执行 - 队列处理" 实现异步任务调度
  1. 阶段控制:在浏览器和 Node.js 中通过不同阶段划分实现场景优化

理解事件循环,就能真正掌握 JavaScript 异步编程的核心:

  • 为什么Promise.then比setTimeout先执行?
  • async/await背后的执行机制是什么?
  • 如何避免 UI 卡顿或 Node.js 事件循环阻塞?

下次遇到异步代码执行顺序问题时,不妨在脑海中画出这个循环图:同步代码→微任务→宏任务→渲染,按照这个顺序拆解,复杂问题就能迎刃而解。

(全文完,欢迎在评论区讨论事件循环的实际应用场景~)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2380136.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux(2)——shell原理及Linux中的权限

目录 一、shell的运行原理 二、Linux中权限的问题 1.权限的概念 2.如何进行用户的切换 1&#xff09;从普通用户切到超级用户 2&#xff09;从root用户切到普通用户 3.如何实现提权操作 4.如何将普通用户添加到信用列表&#xff08;sudoers&#xff09; ​编辑5.Lin…

如何在线免费压缩PDF文档?

PDF文件太大&#xff0c;通常是因为内部嵌入字体和图片。怎么才能将文件大小减减肥呢&#xff0c;主要有降低图片清晰度和去除相关字体两个方向来实现文档效果。接下来介绍三个免费压缩PDF实用工具。 &#xff08;一&#xff09;iLoveOFD在线转换工具 iLoveOFD在线转换工具&a…

汽车装配又又又升级,ethernetip转profinet进阶跃迁指南

1. 场景描述&#xff1a;汽车装配线中&#xff0c;使用EtherNet/IP协议的机器人与使用PROFINET协议的PLC进行数据交互。 2. 连接设备&#xff1a;EtherNet/IP机器人控制器&#xff08;如ABB、FANUC&#xff09;与PROFINET PLC&#xff08;如西门子S7-1500&#xff09;。 3. 连…

css:无限滚动波浪线

以上是需要实现的效果&#xff0c;一条无限滚动波浪线&#xff0c;可以用来做区块的分割线。 要形成上下交替的圆形&#xff0c;思路是给div加圆角边框&#xff0c;第一个只有上边框&#xff0c;第二个只有下边框。 循环了100个div&#xff0c;这个数量根据自己容器宽度调整&…

w~自动驾驶~合集3

我自己的原文哦~ https://blog.51cto.com/whaosoft/13269720 #FastOcc 推理更快、部署友好Occ算法来啦&#xff01; 在自动驾驶系统当中&#xff0c;感知任务是整个自驾系统中至关重要的组成部分。感知任务的主要目标是使自动驾驶车辆能够理解和感知周围的环境元素&…

山东大学计算机图形学期末复习整理5——CG10上

CG10上 Frenet-Serret框架 空间中一条曲线可以写成参数形式&#xff1a; C ( u ) ( x ( u ) , y ( u ) , z ( u ) ) \mathbf{C}(u) (x(u), y(u), z(u)) C(u)(x(u),y(u),z(u)) 这表示&#xff1a;当参数 u u u 变化时&#xff0c;曲线在三维空间中移动&#xff0c;生成一条轨…

STM32移植LVGL8.3 (保姆级图文教程)

目录 前言设备清单2.8寸TFT-LCD屏原理与应用1️⃣基本参数2️⃣引脚说明3️⃣程序移植4️⃣硬件接线 LVGL8.3 移植流程1️⃣硬件及平台要求2️⃣版本说明3️⃣源码下载4️⃣源码移植 工程配置修改配置文件1️⃣lvgl_config.h2️⃣适配屏幕驱动3️⃣配置输入设备(触摸功能) 提供…

虚幻引擎5-Unreal Engine笔记之Default Pawn与GamMode、Camera的关系

虚幻引擎5-Unreal Engine笔记之Default Pawn与GamMode、Camera的关系 code review! 文章目录 虚幻引擎5-Unreal Engine笔记之Default Pawn与GamMode、Camera的关系1.Default Pawn与Camera的关系1.1. Default Pawn 是什么&#xff1f;1.2. Default Pawn 的主要组件1.3. Default…

C++多态的详细讲解

【本节目标】 1. 多态的概念 2. 多态的定义及实现 3. 抽象类 4. 多态的原理 5. 单继承和多继承关系中的虚函数表 前言 需要声明的&#xff0c;本博客中的代码及解释都是在 vs2013 下的 x86 程序中&#xff0c;涉及的指针都是 4bytes 。 如果要其他平台下&#xff0c;部…

vue项目启动报错

vue项目启动报错 一、问题二、解决 一、问题 从vue2更换到vue3之后&#xff0c;需要将node进行版本升级&#xff0c;之后启动项目出现了下面的问题。 Uncaught Error: A route named “PageNotFound” has been added as a child of a route with the same name. Route names …

免费私有化部署! PawSQL社区版,超越EverSQL的企业级SQL优化工具面向个人开发者开放使用了

1. 概览 1.1 快速了解 PawSQL PawSQL是专注于数据库性能优化的企业级工具&#xff0c;解决方案覆盖SQL开发、测试、运维的整个流程&#xff0c;提供智能SQL审核、查询重写优化及自动化巡检功能&#xff0c;支持MySQL、PostgreSQL、Oracle、SQL Server等主流数据库及达梦、金仓…

SecureCRT 使用指南:安装、设置与高效操作

目录 一、SecureCRT 简介 1.1 什么是 SecureCRT&#xff1f; 1.2 核心功能亮点 1.3 软件特点 二、SecureCRT 安装与激活 2.1 安装步骤&#xff08;Windows 系统&#xff09; 2.2 激活与破解&#xff08;仅供学习参考&#xff09; 三、基础配置与优化 3.1 界面与编码设…

Tomcat多应用部署与静态资源路径问题全解指南

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家、CSDN平台优质创作者&#xff0c;高级开发工程师&#xff0c;数学专业&#xff0c;10年以上C/C, C#, Java等多种编程语言开发经验&#xff0c;拥有高级工程师证书&#xff1b;擅长C/C、C#等开发语言&#xff0c;熟悉Java常用开…

【微信小程序 + 高德地图API 】键入关键字搜索地址,获取经纬度等

前言 又到熟悉的前言&#xff0c;接到个需求&#xff0c;要引入高德地图api&#xff0c;我就记录一下&#xff0c;要是有帮助记得点赞、收藏、关注&#x1f601;。 后续有时间会慢慢完善一些文章&#xff1a;&#xff08;画饼时间&#xff09; map组件自定义气泡、mark标记点…

排序算法之线性时间排序:计数排序,基数排序,桶排序详解

排序算法之线性时间排序&#xff1a;计数排序、基数排序、桶排序详解 前言一、计数排序&#xff08;Counting Sort&#xff09;1.1 算法原理1.2 代码实现&#xff08;Python&#xff09;1.3 性能分析1.4 适用场景 二、基数排序&#xff08;Radix Sort&#xff09;2.1 算法原理2…

Linux | mdadm 创建软 RAID

注&#xff1a;本文为 “Linux mdadm RAID” 相关文章合辑。 略作重排&#xff0c;未整理去重。 如有内容异常&#xff0c;请看原文。 Linux 下用 mdadm 创建软 RAID 以及避坑 喵ฅ・&#xfecc;・ฅ Oct 31, 2023 前言 linux 下组软 raid 用 mdadm 命令&#xff0c;multi…

CodeEdit:macOS上一款可以让Xcode退休的IDE

CodeEdit 是一款轻量级、原生构建的代码编辑器&#xff0c;完全免费且开源。它使用纯 swift 实现&#xff0c;而且专为 macOS 设计&#xff0c;旨在为开发者提供更高效、更可靠的编程环境&#xff0c;同时释放 Mac 的全部潜力。 Stars 数21,719Forks 数1,081 主要特点 macOS 原…

LLaMA-Factory 微调 Qwen2-7B-Instruct

一、系统环境 使用的 autoDL 算力平台 1、下载基座模型 pip install -U huggingface_hub export HF_ENDPOINThttps://hf-mirror.com # &#xff08;可选&#xff09;配置 hf 国内镜像站huggingface-cli download --resume-download shenzhi-wang/Llama3-8B-Chinese-Chat -…

mac本地docker镜像上传指定虚拟机

在Mac本地将Docker镜像上传至指定虚拟机的完整步骤 1. 在Mac本地保存Docker镜像为文件 通过docker save命令将镜像打包为.tar文件&#xff0c;便于传输至虚拟机。 # 示例&#xff1a;保存名为"my_image"的镜像到当前目录 docker save -o my_image.tar my_image:ta…

从代码学习深度学习 - 风格迁移 PyTorch版

文章目录 前言方法 (Methodology)阅读内容和风格图像预处理和后处理抽取图像特征定义损失函数内容损失 (Content Loss)风格损失 (Style Loss)全变分损失 (Total Variation Loss)总损失函数初始化合成图像训练模型总结前言 大家好!欢迎来到我们的深度学习代码学习系列。今天,…