JavaScript 的事件循环

JavaScript 的事件循环(Event Loop)是它实现异步非阻塞操作的核心机制。即使 JavaScript 是单线程的(一次只能做一件事),事件循环也能让它处理定时器、网络请求、用户点击等并发任务。

1. 三大核心部件(内存模型)

调用栈(Call Stack):同步任务的执行环境。它是一个 LIFO(后进先出)结构,记录当前代码执行到哪一步(函数调用进栈,函数返回出栈)。

Web APIs(或 Node.js APIs):浏览器或 Node 提供的外部环境(如 setTimeout、ajax、DOM 事件、Promise 的底层 IO)。当这些异步任务被触发时,它们会被扔到这里等待,不会阻塞栈。

任务队列(Task Queues):异步任务完成后的回调函数存放处,等待被拉回栈中执行。它分为 宏任务队列 和 微任务队列。

2. 核心机制:一次完整的"事件循环"Tick

事件循环会永远不停地检查:"调用栈是否为空?" 如果为空,它就会从任务队列中取出任务放入栈中执行。

一个完整的循环(Tick)执行顺序极其严格:

  1. 先清空当前调用栈(执行所有同步代码)。
  2. 清空微任务队列(Microtasks)------ 全部执行,执行过程中新加的微任务也要当场清掉,直到队列为空。
  3. UI渲染
  4. 执行一次宏任务(Macrotask)------ 仅取出队列头部的一个任务执行。
  5. 回到步骤 2(下一次循环)。

3. 宏任务(Macrotask)和 微任务(Microtask)

宏任务(Macrotask)和 微任务(Microtask)的优先级不同:

微任务(更高优先级):Promise.then()、catch()、finally()、MutationObserver(浏览器)、queueMicrotask()、process.nextTick(Node.js)。

宏任务(较低优先级):setTimeout、setInterval、setImmediate(Node.js)、I/O 操作、UI 渲染、MessageChannel。

  1. setTimeout 和 setInterval 的回调函数会被放入宏任务队列。
  2. async 关键字的作用只有一个:确保该函数隐式返回一个 Promise
  3. await 表达式会暂停异步函数的执行,直到 Promise 被解决,并返回结果。await 之后的代码相当于在 Promise 的 .then() 中执行,会被放入微任务队列。此时函数暂时挂起,控制权交还给主线程
  4. 微任务队列遵循"先进先出(FIFO)"原则
  5. 同源任务,严格按入队顺序执行,比如两个 setTimeout、或两个 click 事件回调,它们严格按照先入先出(FIFO)的顺序执行。
  6. 复杂规则:跨源任务,浏览器有"隐含优先级":
  • 用户交互队列(click、mousedown、键盘事件)
  • 定时器队列(setTimeout、setInterval)
  • 网络请求队列(fetch、xhr 回调)
  • I/O 队列(IndexedDB、文件读写)

用户交互(UI Events) > 网络回调(Network) > 定时器(Timers) > 空闲回调(Idle)

4.代码

bash 复制代码
console.log('1'); // 同步

setTimeout(() => {
  console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // 微任务
});

console.log('4'); // 同步

执行流程:

输出 1(同步栈)。

遇到 setTimeout,回调放入宏任务队列。

遇到 Promise.then,回调放入微任务队列。

输出 4(同步栈清空)。

微任务优先:执行微任务队列,输出 3。

宏任务执行:执行第一个宏任务,输出 2。

最终结果:1 -> 4 -> 3 -> 2。

宏任务中嵌套微任务

bash 复制代码
setTimeout(() => { // 宏任务A
  console.log('A');
  Promise.resolve().then(() => console.log('B')); // 微任务
}, 0);

setTimeout(() => { // 宏任务C
  console.log('C');
}, 0);

结果:A -> B -> C。

原因:执行宏任务 A 时,调用栈重新占据主线程。A 执行完后,事件循环会再次检查微任务队列(发现有 B),清空 B 后,才会去取下一个宏任务 C。

5. 浏览器 , Node.js 的细微差别

  • 浏览器:一次循环只取 一个 宏任务执行,然后立即清空所有微任务。
  • Node.js(旧版):微任务会在事件循环的各个阶段之间执行。
  • Node.js(新版,与浏览器对齐):基本遵循浏览器规则,但多了 nextTick(优先级高于 Promise)。

7.避坑

  • 避免长循环阻塞UI:尽量不要在微任务中递归执行耗时代码(因为微任务会连续清空,这会卡死主线程,导致页面无响应)。如果必须大量计算,建议使用宏任务(setTimeout)来分片执行,或者使用 requestIdleCallback。
  • setTimeout(fn, 0) 不是 0ms:即便设置为 0,浏览器内部也有最小延迟(HTML 标准规定至少
    4ms),且必须等待当前同步代码和微任务全部执行完才会触发。

当前 Script(同步) > process.nextTick > 微任务(Promise) > 宏任务(setTimeout) > 下一次渲染。

相关推荐
星星落进兜里1 小时前
Java虚拟机面试题-补充
java·开发语言
单线程_011 小时前
从案例分析 Vue3 Tokenizer+Parser 源码四
前端·javascript·vue.js
吴声子夜歌4 小时前
ApacheCommons——commons-pool2(高性能通用对象池框架)
java·开发语言·apache
名字还没想好☜9 小时前
React 实现暗黑模式切换:localStorage 持久化、SSR 首屏闪烁与跟随系统主题
前端·javascript·react.js·ecmascript·react·next.js
kruptos9 小时前
分布式系统怎么“选主“?Raft 共识一次讲清
开发语言·分布式·php·共识算法
吴声子夜歌10 小时前
ApacheCommons——commons-cli(命令行参数解析)
java·开发语言·apache
自动化监测Learner10 小时前
主流 Web 端地图引擎对比:选型指南与优劣分析
javascript
benchmark_cc11 小时前
REST API 和 Python SDK 应该怎么选?量化交易数据接口选型实战
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
王的宝库11 小时前
Go 项目结构:从单文件到标准工程布局
开发语言·后端·golang