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

< JS事件循环系列【四】> 事件循环补充概念:从执行细节到性能优化

在之前的内容中,我们已经掌握了事件循环的核心三要素(调用栈、微任务、宏任务),但在实际开发与面试中,还会遇到几个与事件循环密切相关的概念 —— 比如执行上下文、任务队列优先级细分、requestAnimationFrame、process.nextTick 等。这些概念不仅能帮我们更深入理解代码执行细节,还能在性能优化、跨环境开发中发挥关键作用。今天就逐一拆解这些 “补充概念”,让你对事件循环的认知更完整。

一、执行上下文:调用栈的 “执行环境” 基础

很多开发者知道 “调用栈存函数”,但很少关注函数执行时的 “环境”—— 这正是执行上下文(Execution Context) 的作用。它是 JS 引擎为函数或全局代码创建的 “执行环境容器”,包含了函数执行所需的所有信息,也是调用栈能正常工作的基础。

1. 执行上下文的核心组成

每个执行上下文都包含三个关键部分,缺一不可:

  • 变量对象(Variable Object,VO):存储当前上下文的变量、函数声明、参数。比如函数内的局部变量、函数参数,全局上下文的 window 对象(浏览器环境)都属于变量对象;

  • 作用域链(Scope Chain):决定当前上下文能访问哪些变量和函数。由当前上下文的变量对象 + 所有父级上下文的变量对象组成,访问变量时会从 “当前层” 向上查找,直到找到或报错;

  • this 绑定(This Binding):确定当前上下文的 this 指向。比如全局上下文的 this 指向 window(浏览器),函数调用时的 this 指向调用者,箭头函数的 this 继承自父上下文。

2. 执行上下文与调用栈的关系

调用栈的 “压栈”“弹栈” 本质是执行上下文的创建与销毁

  1. 执行全局代码时,JS 引擎创建 “全局执行上下文”,压入调用栈底部;

  2. 调用函数时,创建 “函数执行上下文”,压入调用栈顶部;

  3. 函数执行完毕,其执行上下文从调用栈弹出,销毁(局部变量随之释放);

  4. 所有代码执行完毕,调用栈中仅剩余全局执行上下文,直到页面关闭才销毁。

实例:执行上下文的创建与销毁
let globalVar = "全局变量"; // 全局执行上下文的变量对象function fn(a) {let localVar = "局部变量"; // 函数执行上下文的变量对象console.log(a + localVar + globalVar); // 作用域链查找:a(当前)→ localVar(当前)→ globalVar(父级)}fn("参数"); // 调用函数,创建fn的执行上下文并压栈

执行流程

  1. 全局代码执行:创建 “全局执行上下文”,压栈,globalVar 存入变量对象;

  2. 调用 fn("参数"):创建 “fn 执行上下文”,压栈,a(参数)、localVar 存入变量对象,this 指向 window

  3. 执行 console.log:通过作用域链依次查找 alocalVarglobalVar,输出结果;

  4. fn 执行完毕:其执行上下文从调用栈弹出,localVara 销毁;

  5. 全局代码执行完毕:仅保留全局执行上下文。

3. 为什么要关注执行上下文?

它是理解 “变量提升”“作用域隔离”“this 指向” 的关键。比如:

  • 变量提升:本质是执行上下文创建时,变量和函数声明被提前存入变量对象;

  • 闭包:函数执行上下文销毁后,其变量对象被父级作用域引用,导致变量未释放;

  • this 指向异常:若不理解执行上下文的 this 绑定规则,容易出现 this 指向 undefinedwindow 的问题。

二、任务队列优先级:不止 “微任务>宏任务”

之前我们简化了 “微任务优先级高于宏任务”,但实际浏览器和 Node.js 中,任务队列的优先级有更细的划分—— 同一类型的任务可能有不同优先级,不同环境的优先级规则也有差异。这也是为什么有时 “同样是微任务,执行顺序却不同” 的原因。

1. 浏览器环境:任务队列优先级细分

浏览器中,任务队列的优先级从高到低可分为以下层级(优先级高的先执行):

  1. 紧急微任务(Immediate Microtasks):如 queueMicrotaskPromise.then,这是我们之前说的 “微任务” 核心;

  2. 动画帧任务(requestAnimationFrame):专门用于页面动画,优先级低于微任务,高于普通宏任务;

  3. 普通宏任务(Normal Macrotasks):如 setTimeoutsetInterval、DOM 事件、fetch 回调;

  4. 空闲回调任务(requestIdleCallback):优先级最低,仅在浏览器空闲时执行(如页面渲染、其他任务都完成后)。

关键差异:requestAnimationFrame 不是微任务 / 宏任务

很多人误以为 requestAnimationFrame 是宏任务,其实它是浏览器专门为动画设计的 “独立任务”,执行时机特殊:

  • 每次浏览器 “重绘前” 执行(约 16.67ms 一次,对应 60fps);

  • 执行时机在 “微任务清空后、普通宏任务执行前”,但仅在需要渲染时触发(比如页面隐藏时不执行)。

实例:任务队列优先级验证
// 1. 紧急微任务Promise.resolve().then(() => console.log("1:Promise微任务"));queueMicrotask(() => console.log("2:queueMicrotask微任务"));// 2. 动画帧任务requestAnimationFrame(() => console.log("3:requestAnimationFrame"));// 3. 普通宏任务setTimeout(() => console.log("4:setTimeout宏任务"), 0);document.addEventListener("click", () => console.log("5:DOM事件宏任务"));// 4. 空闲回调任务requestIdleCallback(() => console.log("6:requestIdleCallback"));console.log("0:同步代码");

执行顺序(点击页面前)012346(浏览器空闲时);

点击页面后:会插入 5,执行顺序为 0123546

解析:微任务先执行,然后是动画帧任务,接着是触发的 DOM 事件宏任务,再是 setTimeout,最后是空闲回调。

2. Node.js 环境:宏任务阶段细分

Node.js 的事件循环与浏览器不同,它将宏任务分为 6 个阶段,每个阶段对应一个 “宏任务队列”,按顺序执行(阶段内队列清空后才进入下一个阶段)。阶段从高到低依次为:

  1. timers 阶段:执行 setTimeoutsetInterval 的回调;

  2. pending callbacks 阶段:执行延迟到下一轮的 I/O 回调(如网络请求、文件操作的错误回调);

  3. idle/prepare 阶段:Node.js 内部使用,开发者无需关注;

  4. poll 阶段:执行 I/O 回调(如 fs.readFilehttp 请求的成功回调),是 Node.js 事件循环的核心阶段;

  5. check 阶段:执行 setImmediate 的回调;

  6. close callbacks 阶段:执行关闭回调(如 socket.on("close", ...))。

关键差异:process.nextTick(Node.js 特有微任务)

Node.js 中有一个 “特殊微任务”——process.nextTick,它的优先级高于普通微任务(如 Promise.then),且不占用 “微任务队列”,而是有独立的 “nextTick 队列”:

  • 每个宏任务阶段执行完毕后,会先清空所有 process.nextTick 任务,再清空普通微任务队列;

  • 即使在微任务中调用 process.nextTick,也会优先于其他微任务执行。

实例:Node.js 任务优先级
// Node.js 环境执行console.log("0:同步代码");// 普通微任务Promise.resolve().then(() => console.log("1:Promise微任务"));// Node.js 特殊微任务process.nextTick(() => console.log("2:process.nextTick"));// 宏任务:timers阶段setTimeout(() => console.log("3:setTimeout(timers阶段)"), 0);// 宏任务:check阶段setImmediate(() => console.log("4:setImmediate(check阶段)"));// I/O 宏任务:poll阶段const fs = require("fs");fs.readFile(filename, () => {console.log("5:fs.readFile(poll阶段)");process.nextTick(() => console.log("6:I/O中的nextTick"));Promise.resolve().then(() => console.log("7:I/O中的Promise微任务"));});

执行顺序

  1. 同步代码 → 0

  2. 清空 nextTick 队列 → 2

  3. 清空普通微任务 → 1

  4. 进入 timers 阶段 → 3

  5. 进入 poll 阶段 → 5

  • 清空 I/O 回调中的 nextTick → 6

  • 清空 I/O 回调中的普通微任务 → 7

  1. 进入 check 阶段 → 4

通过这个实例能清晰看到:Node.js 中 process.nextTick 优先级最高,且宏任务按 “阶段顺序” 执行,与浏览器的规则差异明显。

三、requestAnimationFrame:动画场景的 “专用任务”

在 “任务队列优先级” 中我们提到了 requestAnimationFrame(简称 rAF),但它的特殊性值得单独拆解。作为浏览器为动画设计的 API,它解决了 “定时器做动画卡顿” 的问题,是前端动画优化的核心工具,也与事件循环的 “渲染时机” 深度绑定。

1. rAF 的核心特性

  • 执行时机:每次浏览器 “重绘前” 触发,频率与显示器刷新率同步(通常 60 次 / 秒,即 16.67ms 一次);

  • 自动停止:页面隐藏(如切换标签页)时,rAF 会暂停执行,避免无用性能消耗;

  • 回调参数:回调函数会接收一个 “时间戳” 参数,表示当前帧的渲染时间,可用于计算动画进度;

  • 优先级:介于 “微任务” 和 “普通宏任务” 之间,微任务清空后、宏任务执行前触发。

2. 为什么定时器做动画不如 rAF?

setTimeout(fn, 16.67) 做动画时,会有两个关键问题,而 rAF 能完美解决:

  • 时间不准setTimeout 的延迟是 “最小延迟”,若主线程被阻塞(如同步代码执行久),动画会卡顿;rAF 由浏览器控制,严格与渲染同步,时间更精准;

  • 后台消耗setTimeout 在页面隐藏后仍会执行,浪费 CPU;rAF 会自动暂停,节省性能。

实例:rAF 实现平滑动画
<div id="box" style="width: 100px; height: 100px; background: red; position: absolute;"></div><script>const box = document.getElementById("box");let position = 0; // 初始位置// 用rAF实现动画function animate(timestamp) {// 更新位置(每帧移动1px)position += 1;box.style.left = position + "px";// 边界判断:未超过屏幕宽度则继续if (position < window.innerWidth - 100) {requestAnimationFrame(animate); // 递归调用,下帧继续执行}}// 启动动画requestAnimationFrame(animate);</script>

优势:动画全程平滑,切换标签页后自动暂停,切回后从当前位置继续,性能消耗远低于定时器。

四、process.nextTick 与 setImmediate:Node.js 特有宏任务

在 Node.js 开发中,process.nextTicksetImmediate 是两个容易混淆的 API,它们虽都属于 “异步任务”,但优先级、执行时机与普通宏任务差异极大,也是面试高频考点。

1. process.nextTick:Node.js 优先级最高的 “微任务”

虽然名字带 “nextTick”,但它不是宏任务,而是 Node.js 设计的 “特殊微任务”,核心特点:

  • 优先级最高:无论在哪个阶段,只要有 process.nextTick 任务,就会在 “当前阶段结束后、下阶段开始前” 立即执行,甚至会阻塞后续阶段(需避免嵌套调用过多导致死循环);

  • 独立队列:不占用普通微任务队列,有自己的 “nextTick 队列”,清空后才处理 Promise 等普通微任务;

  • 应用场景:用于 “在当前任务结束后、下任务开始前” 执行逻辑,如修改对象属性、触发事件等。

注意:避免 process.nextTick 嵌套

若在 process.nextTick 中嵌套调用 process.nextTick,会导致 “nextTick 队列永远不清空”,阻塞事件循环,比如:

// 错误示例:无限嵌套,导致事件循环阻塞process.nextTick(() => {console.log("nextTick");process.nextTick(arguments.callee); // 递归调用,永远执行不完});

2. setImmediate:Node.js 宏任务的 “check 阶段专属”

setImmediate 是 Node.js 特有的宏任务,仅在 “check 阶段” 执行,核心特点:

  • 执行时机:在 poll 阶段结束后触发,优先级低于 timers 阶段的 setTimeout,但高于 close callbacks 阶段;

  • 与 setTimeout (0) 的差异:在 I/O 回调中,setImmediate 一定比 setTimeout(0) 先执行(见前文 Node.js 场景案例);在全局代码中,两者顺序不确定;

  • 应用场景:用于 “在 I/O 操作完成后,立即执行后续逻辑”,比 setTimeout(0) 更精准。

实例:setImmediate 与 setTimeout 在 I/O 中的顺序
// Node.js 环境const fs = require("fs");// I/O 回调中,setImmediate 先于 setTimeout(0) 执行fs.readFile(filename, () => {setTimeout(() => console.log("setTimeout"), 0);setImmediate(() => console.log("setImmediate"));});

执行顺序setImmediatesetTimeout,因为 I/O 回调在 poll 阶段执行,之后直接进入 check 阶段(执行 setImmediate),再进入下一轮 timers 阶段(执行 setTimeout)。

五、总结:事件循环的完整概念体系

到这里,我们已经覆盖了事件循环的 “核心概念 + 补充概念”,可以梳理出一个完整的体系:

  1. 基础支撑:执行上下文(调用栈的执行环境);

  2. 核心任务

  • 微任务:普通微任务(Promise/queueMicrotask)、Node.js 特殊微任务(process.nextTick);

  • 宏任务:浏览器宏任务(setTimeout/DOM 事件 /fetch)、Node.js 分阶段宏任务(timers/poll/check 等阶段);

  1. 特殊任务:浏览器动画任务(requestAnimationFrame)、浏览器空闲任务(requestIdleCallback);

  2. 跨环境差异:Node.js 有 process.nextTick/setImmediate,浏览器有 rAF/requestIdleCallback,任务优先级规则不同。

理解这些概念后,不仅能轻松解决 “代码执行顺序” 问题,还能在实际开发中:

  • 用 rAF 优化动画性能;

  • 用 process.nextTick/setImmediate 处理 Node.js 异步逻辑;

  • 避免任务队列优先级导致的 “执行顺序 bug”;

  • 利用 requestIdleCallback 处理低优先级任务(如日志上报)。

如果在某个概念的应用场景或执行时机上还有疑问,不妨结合具体代码案例测试 —— 事件循环的理解,终究需要 “理论 + 实践” 的结合,多动手、多分析,才能真正掌握!


文章转载自:

http://985Jn6FW.gyfzp.cn
http://A8m09Ido.gyfzp.cn
http://hh5NgaWG.gyfzp.cn
http://e8EVTfg1.gyfzp.cn
http://54UyWTSk.gyfzp.cn
http://OFDpKQBB.gyfzp.cn
http://wGvGbhAL.gyfzp.cn
http://oUxYdCDo.gyfzp.cn
http://4IstAhFr.gyfzp.cn
http://C9kOgMmt.gyfzp.cn
http://dd2JSSrG.gyfzp.cn
http://jxPkexur.gyfzp.cn
http://RYMCVkrt.gyfzp.cn
http://8ij6dAHq.gyfzp.cn
http://I8UpLFT8.gyfzp.cn
http://TB5wXB2Q.gyfzp.cn
http://ssR4qBd2.gyfzp.cn
http://VG9l293H.gyfzp.cn
http://YySFC4zS.gyfzp.cn
http://m4z0Yfls.gyfzp.cn
http://jHi2zxXa.gyfzp.cn
http://dOD1SKT3.gyfzp.cn
http://6ZNEVrUz.gyfzp.cn
http://OoJXtOyY.gyfzp.cn
http://BS3D21yw.gyfzp.cn
http://UhnbDRAr.gyfzp.cn
http://DepJ1L0W.gyfzp.cn
http://ON5A13At.gyfzp.cn
http://6R1G7RkI.gyfzp.cn
http://CGbyJQLJ.gyfzp.cn
http://www.dtcms.com/a/383933.html

相关文章:

  • MySQL从入门到精通:基础、安装与实战管理指南
  • 解决:Ubuntu、Kylin、Rocky系统中root用户忘记密码
  • javascript文本长度检测与自动截取,用于标题长度检测
  • 解锁 DALL・E 3:文生图多模态大模型的无限可能
  • 深入理解 LVS-DR 模式与 Keepalived 高可用集群
  • 数据库学习MySQL系列4、工具一 Navicat Premium 图形化软件的使用详细教程
  • RL【10-2】:Actor - Critic
  • MATLAB学习文档(十六)
  • 滑动窗口概述
  • 【C++语法】模版初阶
  • 机械制造工艺指南
  • Wi-Fi技术——Power SAVE模式
  • leetcode39(相同的树)
  • C++(虚函数表原理和菱形继承)
  • 【STM32项目开源】STM32单片机智能语音风扇控制系统
  • [Android]自定义view
  • 线程和进程,以及GCD的简单使用
  • C++_STL和数据结构《1》_STL、STL的迭代器、c++中的模版、STL的容器、列表初始化、三个算法、链表
  • 学习日报|线程池专题学习总结
  • kubectl 报错 couldn‘t get current server API group list:
  • 求最小公倍数(GCD)和最大公约数(LCM)——原理和代码
  • 单调栈数据结构
  • OceanBase V4.3.5 BP3版本Bug:DROP TABLE删表会卡住
  • KDTS迁移工具全流程实战教程:从安装配置到增量同步
  • 苹果本装win10记
  • 电子科学与技术专业考研专业和学校确定
  • 模电基础:三极管的基本原理
  • 【Ambari监控】Sqlline 启动卡死问题处理
  • Day 03 设置粒子枪 G4ParticleGun -----以B1为实例
  • AI论文写作工具的利弊分析:如何高效利用与规避风险