JavaScript运行机制、v8原理、js事件循环

文章目录

  • 一、有了解过JavaScript引擎吗?JavaScript运行机制有没有详细了解过?请详细说明
    • [1. JavaScript是解释型语言还是编译型语言?](#1. JavaScript是解释型语言还是编译型语言?)
      • [1.1 编译型 vs. 解释型:核心差异](#1.1 编译型 vs. 解释型:核心差异)
        • [**编译型语言 (Compiled)**](#编译型语言 (Compiled))
        • [**解释型语言 (Interpreted)**](#解释型语言 (Interpreted))
      • [1.2 JavaScript 属于哪一种?](#1.2 JavaScript 属于哪一种?)
      • [1.3 什么是 JIT(即时编译)?](#1.3 什么是 JIT(即时编译)?)
        • [**JIT 的工作流程:**](#JIT 的工作流程:)
      • [1.4 核心特性对比表](#1.4 核心特性对比表)
    • [2. 深度解析:JavaScript 引擎 (JS Engine)](#2. 深度解析:JavaScript 引擎 (JS Engine))
      • [2.1 什么是 JavaScript 引擎?](#2.1 什么是 JavaScript 引擎?)
      • [2.2 主流引擎概览](#2.2 主流引擎概览)
      • [2.3 引擎内部是如何工作的? (以 V8 为例)](#2.3 引擎内部是如何工作的? (以 V8 为例))
      • [2.4 为什么要理解引擎原理?](#2.4 为什么要理解引擎原理?)
    • [3. 深度解析:浏览器引擎 (Browser Engine / Rendering Engine)](#3. 深度解析:浏览器引擎 (Browser Engine / Rendering Engine))
      • [3.1 什么是浏览器引擎?](#3.1 什么是浏览器引擎?)
      • [3.2 三足鼎立:主流引擎分布](#3.2 三足鼎立:主流引擎分布)
      • [3.3 渲染流水线 (Rendering Pipeline)](#3.3 渲染流水线 (Rendering Pipeline))
      • [3.4 浏览器引擎 vs. JS 引擎 的协作](#3.4 浏览器引擎 vs. JS 引擎 的协作)
      • [3.5 开发者为何必须掌握它?](#3.5 开发者为何必须掌握它?)
    • [4. 深度解析:V8 引擎如何执行 JavaScript 代码](#4. 深度解析:V8 引擎如何执行 JavaScript 代码)
      • [4.1 解析阶段 (Parsing)](#4.1 解析阶段 (Parsing))
        • [**词法分析 (Scanner)**:](#词法分析 (Scanner):)
        • [**语法分析 (Parser)**:](#语法分析 (Parser):)
      • [4.2 解释阶段 (Interpretation)](#4.2 解释阶段 (Interpretation))
      • [4.3 监控与分析 (Profiling)](#4.3 监控与分析 (Profiling))
      • [4.4 优化编译 (JIT Compilation)](#4.4 优化编译 (JIT Compilation))
      • [4.5 去优化 (Deoptimization)](#4.5 去优化 (Deoptimization))
      • [🚀 总结:V8 的执行流水线](#🚀 总结:V8 的执行流水线)
    • [5 JS 引擎的运行机制与环境隔离](#5 JS 引擎的运行机制与环境隔离)
      • [5.1 为什么"解释型"语言也需要先"扫描"代码?](#5.1 为什么“解释型”语言也需要先“扫描”代码?)
      • [5.2 环境隔离:变量环境 vs. 词法环境](#5.2 环境隔离:变量环境 vs. 词法环境)
        • [**变量环境 (Variable Environment)**](#变量环境 (Variable Environment))
        • [**词法环境 (Lexical Environment)**](#词法环境 (Lexical Environment))
      • [5.3 "物理隔离"带来的深远影响](#5.3 “物理隔离”带来的深远影响)
      • [🚀 核心总结表](#🚀 核心总结表)
  • [二、JavaScript 是单线程还是多线程?请问异步任务处理机制是怎么样的?分别说明浏览器与 Node 的事件循环机制](#二、JavaScript 是单线程还是多线程?请问异步任务处理机制是怎么样的?分别说明浏览器与 Node 的事件循环机制)
    • [1. 核心定性:JavaScript 到底是不是单线程?](#1. 核心定性:JavaScript 到底是不是单线程?)
    • [2. 异步任务全家桶 (全面清单)](#2. 异步任务全家桶 (全面清单))
      • [**微任务 (Microtask) ------ 优先级最高**](#微任务 (Microtask) —— 优先级最高)
      • [**宏任务 (Macrotask) ------ 优先级次之**](#宏任务 (Macrotask) —— 优先级次之)
    • [3. 事件循环 (Event Loop) 执行模型](#3. 事件循环 (Event Loop) 执行模型)
    • [4. 高频面试避坑指南 (Killer Points)](#4. 高频面试避坑指南 (Killer Points))
      • [**Q1:Promise 内部是异步的吗?**](#Q1:Promise 内部是异步的吗?)
      • [**Q2:await 后面代码的执行顺序?**](#Q2:await 后面代码的执行顺序?)
    • [5. 异步任务调度题](#5. 异步任务调度题)
      • [5.1 第一轮:执行同步代码(第一个宏任务)](#5.1 第一轮:执行同步代码(第一个宏任务))
      • [5.2 第二轮:清空微任务队列(核心环节)](#5.2 第二轮:清空微任务队列(核心环节))
      • [5.3 第三轮:开始执行宏任务](#5.3 第三轮:开始执行宏任务)
      • [5.4 🚀 最终输出顺序结果](#5.4 🚀 最终输出顺序结果)

一、有了解过JavaScript引擎吗?JavaScript运行机制有没有详细了解过?请详细说明

1. JavaScript是解释型语言还是编译型语言?

1.1 编译型 vs. 解释型:核心差异

我们可以把编程语言想象成一份外文食谱,为了让计算机(只会二进制)读懂,我们需要不同的翻译方式:

编译型语言 (Compiled)
  • 过程 :在程序运行之前,先由编译器 (Compiler) 将整个源代码一次性翻译成机器语言(如 Windows 下的 .exe)。
  • 代表:C、C++、Go、Rust。
  • 特点
    • 运行快:运行时无需翻译,直接执行机器码。
    • 不灵活:即使改动一行代码,也需要重新编译整个程序。
    • 平台依赖:在 Windows 上编译的程序通常无法直接在 Linux 上运行。
解释型语言 (Interpreted)
  • 过程 :程序运行时,由解释器 (Interpreter) 一行一行地读取源代码,"翻译"一行就"执行"一行。
  • 代表:Python、Ruby、早期的 JavaScript。
  • 特点
    • 运行慢:边译边跑,翻译过程会占用实际运行时间。
    • 灵活:跨平台性好,只要系统安装了对应的解释器,代码即可运行。

1.2 JavaScript 属于哪一种?

结论:现代 JavaScript 是一种采用 JIT (Just-In-Time) 即时编译技术的动态语言。

虽然传统上 JS 被视为"解释型脚本语言",但现代 JS 引擎(如 Chrome 的 V8)为了极致的性能,早已进化为混合模式。它不再是单纯地逐行翻译,而是通过 JIT 技术在运行过程中动态优化代码。


1.3 什么是 JIT(即时编译)?

JIT 结合了编译型和解释型的优点,旨在解决"解释器太慢"和"编译器启动久"的痛点。

JIT 的工作流程:
  1. 快速响应 :代码加载时,解释器首先介入,快速开始执行,让用户感知不到延迟。
  2. 热点探测 :在运行过程中,引擎会监控哪些代码块被频繁执行(称为 Hot Spot / 热点代码)。
  3. 即时编译JIT 编译器 将这些热点代码直接编译为高效的机器码
  4. 替换执行:当再次遇到相同代码时,直接调用编译好的机器码,跳过解释步骤。

性能 ≈ 解释器的响应速度 + 编译器的执行效率 性能 \approx 解释器的响应速度 + 编译器的执行效率 性能≈解释器的响应速度+编译器的执行效率


1.4 核心特性对比表

特性 编译型 (Compiled) 解释型 (Interpreted) JIT (现代 JS / JVM)
翻译时机 程序运行前 运行时 (逐行翻译) 运行时 (按需编译)
执行速度 极快 较慢 接近原生速度
启动速度 慢 (需等待编译完成)
跨平台性 较低 (需重新编译) 极高
典型代表 C++, Rust, Go Python, PHP JavaScript (V8), Java

💡 进阶知识:

现代 JS 引擎甚至拥有 "去优化 (Deoptimization)" 机制。如果 JIT 编译器根据之前的运行数据做出了错误的优化假设(例如本以为某个变量总是数字,结果突然变成了字符串),引擎会立即丢弃已优化的机器码,回退到解释器模式,以确保程序的正确性。

2. 深度解析:JavaScript 引擎 (JS Engine)

2.1 什么是 JavaScript 引擎?

JavaScript 引擎是一个专门负责解析、解释并执行 JavaScript 代码的程序。

如果把浏览器比作一辆汽车,那么 JavaScript 引擎就是这辆车的发动机。它的核心任务是:将人类可读的高级代码(JS)转换为计算机 CPU 能够理解并运行的二进制机器指令。


2.2 主流引擎概览

不同的环境和浏览器使用不同的引擎,但它们都遵循 ECMAScript 标准:

引擎名称 开发者 主要应用环境
V8 Google Chrome, Node.js, Electron, Edge
SpiderMonkey Mozilla Firefox
JavaScriptCore Apple Safari, iOS 全线应用
Chakra Microsoft 早期 Edge, IE (已逐步退出舞台)

2.3 引擎内部是如何工作的? (以 V8 为例)

现代引擎的工作流程并不是简单的"翻译",而是一个复杂的流水线:

  1. 解析 (Parsing)
    • 引擎将源码拆解为 Tokens (记号)。
    • 生成 AST (Abstract Syntax Tree, 抽象语法树),这是代码的结构化表示。
  2. 解释 (Interpretation)
    • 解释器(V8 中的 Ignition )将 AST 转换为中间形态的 字节码 (Bytecode) 并开始执行。这一步保证了代码能以最快速度启动。
  3. 编译与优化 (JIT Compilation)
    • 引擎会监控运行状态。如果某段代码运行非常频繁(Hot Spot / 热点代码 ),编译器(V8 中的 TurboFan )会将其直接编译为高性能的机器码
  4. 垃圾回收 (Garbage Collection)
    • 引擎内置管理机制,自动识别并释放不再使用的内存空间。

2.4 为什么要理解引擎原理?

  • 性能优化:了解引擎如何识别"热点代码",可以避免写出触发"去优化 (Deoptimization)"的代码(例如频繁改变对象属性结构)。
  • 内存管理:理解引擎如何分配内存,能更好地规避内存泄漏问题。
  • 底层视野:这是从"调包侠"迈向"架构师/高级工程师"的必经之路,也是技术面试(尤其是字节、腾讯等大厂)的常考内容。

💡 核心要点:

JavaScript 引擎并不是独立的,它运行在宿主环境 (如浏览器或 Node.js)中。引擎只负责执行 JS,而 DOM 操作、网络请求(AJAX)、定时器(setTimeout)是由宿主环境提供的 Web APIs 或内置模块处理的。

3. 深度解析:浏览器引擎 (Browser Engine / Rendering Engine)

3.1 什么是浏览器引擎?

如果说 JS 引擎 是汽车的"发动机",那么 浏览器引擎(也称渲染引擎)就是整台车的"底盘与组装车间"。

它的核心职责是:读取 HTML、CSS 和图像资源,经过一系列复杂的计算,最终将网页内容像素化并绘制在用户的屏幕上。


3.2 三足鼎立:主流引擎分布

目前市面上绝大多数浏览器都基于以下三大引擎构建:

引擎名称 主要开发者 代表浏览器 特点
Blink Google / 社区 Chrome, Edge, Opera WebKit 的分支,目前生态位最强,性能优异。
WebKit Apple Safari, 所有 iOS 浏览器 注重能效比,是苹果生态系统的唯一准入引擎。
Gecko Mozilla Firefox 坚持独立开发,高度尊重隐私与 Web 标准。

3.3 渲染流水线 (Rendering Pipeline)

浏览器引擎将代码转化为图像的过程被称为 关键渲染路径 (Critical Rendering Path)

  1. 解析 (Parsing)
    • 解析 HTML → 生成 DOM 树
    • 解析 CSS → 生成 CSSOM 树
  2. 构建渲染树 (Render Tree)
    • 将 DOM 与 CSSOM 合并。引擎会过滤掉不需要显示的元素(如 display: none)。
  3. 布局 (Layout / Reflow)
    • 计算每个节点在屏幕上的确切几何位置(高、宽、坐标)。
  4. 绘制 (Painting)
    • 将渲染树中的每个节点转换成屏幕上的实际像素点。
  5. 合成 (Compositing)
    • 将网页的各个图层(Layers)按正确顺序叠加,生成最终图像。

3.4 浏览器引擎 vs. JS 引擎 的协作

两者虽各司其职,但在运行过程中紧密配合:

  • 渲染中断 :当浏览器引擎解析 HTML 遇到 <script> 标签时,会暂停渲染,等待 JS 引擎 执行完脚本。
  • 双向通信 :JS 引擎通过 DOM API 修改网页内容,浏览器引擎接收到指令后触发 重排(Reflow)重绘(Repaint)

3.5 开发者为何必须掌握它?

  • 性能调优:理解布局(Layout)比绘制(Paint)更耗性能,可以减少不必要的页面卡顿。
  • 解决兼容性 :了解不同引擎对 CSS 特性的实现差异(如 -webkit- 前缀的由来)。
  • 面试深度:它是大厂面试题"从输入 URL 到页面显示发生了什么"的核心环节。

💡 黄金公式:

浏览器 (Browser) = 浏览器引擎 (Blink/WebKit) + JS 引擎 (V8/JSC) + 网络模块 + UI 界面 + 各类 Web API。

4. 深度解析:V8 引擎如何执行 JavaScript 代码

4.1 解析阶段 (Parsing)

当 V8 接收到源码字符串后,会进行两步预处理:

词法分析 (Scanner)

词法分析是解析的第一步,它的核心目标是:"识字"并"切分"

  • 切分单词(Tokenizing):将一连串的源码字符流拆分成一个个具有独立语义的单元,称为 Token(词法单元)。

    • 例如:let a = 10; 会被拆分为 let (关键字), a (变量名), = (赋值符), 10 (数字), ; (分隔符)。
  • 过滤杂质:自动剔除代码中对逻辑运行无意义的内容,如空格、换行符、注释等。

  • 初步分类与转换:将字符串转换为内部编号(ID)。对于引擎来说,处理数字 1(代表关键字 let)比处理字符串 "let" 要快得多。

  • 词法错误检查:发现不符合词法规则的字符。例如在 JS 中写了一个非法的特殊符号,词法分析阶段就会报错。

语法分析 (Parser)

语法分析是解析的第二步,它的核心目标是:"组句"并"建树"。

  • 构建 AST(抽象语法树):将词法分析产出的平铺的 Token 序列,根据语言的语法规则(文法)组装成一棵树状结构。这棵树展示了代码之间的层级和逻辑关系。

    • 例如:它会识别出 a = 10 是一个"赋值表达式",其中 a 是左值,10 是右值。
  • 验证语法合法性:检查 Token 的排列顺序是否符合 JS 语法。

    • 词法分析能认出 let、=、;,但只有语法分析能告诉你 let = ; 是错误的排布。
  • 确定作用域与语义:在建树的过程中,解析器会初步确定变量的作用域(全局还是局部),并为后续生成字节码提供逻辑依据。

4.2 解释阶段 (Interpretation)

  • 角色Ignition 解释器
  • 动作 :将 AST 转换为 Bytecode (字节码) 并立即开始执行。
  • 优势:字节码生成速度极快,且比机器码占用更少的内存,保证了网页的"首屏加载速度"。

4.3 监控与分析 (Profiling)

  • 在代码运行期间,V8 会启动一个 Profiler 监听运行状态。
  • 它会寻找那些被多次调用的函数或循环,将其标记为 "Hot Spot" (热点代码)

4.4 优化编译 (JIT Compilation)

  • 角色TurboFan 编译器
  • 动作 :将"热点代码"的字节码直接编译为 Optimized Machine Code (优化机器码)
  • 结果:机器码是二进制指令,CPU 直接读取执行,运行速度接近 C++ 原生水平。

4.5 去优化 (Deoptimization)

  • 原理:JS 是动态类型语言。如果 TurboFan 假设某个变量一直是数字并进行了优化,但运行中它突然变成了字符串。
  • 处理 :引擎会立即撤销优化(Deopt),回退到 Ignition 解释器执行字节码,确保逻辑正确性。

🚀 总结:V8 的执行流水线

状态 处理过程 产物 特点
初始 源码读取 字符串 人类可读
分析 Parsing AST 树 逻辑结构化
启动 Ignition 字节码 响应快、内存省
加速 TurboFan 机器码 执行快、性能高

💡 开发者启示

了解这个过程后,你会发现:保持变量类型的一致性(不要随意改变对象属性的类型或结构)能显著减少"去优化"的发生,让代码始终运行在 TurboFan 的"高速公路"上。

5 JS 引擎的运行机制与环境隔离

5.1 为什么"解释型"语言也需要先"扫描"代码?

虽然 JavaScript 是即时编译(JIT)语言,但它在执行前必须经过 Parser(解析器) 的全量扫描。

  • 语法安全检查 :在代码运行前发现 SyntaxError(如括号不匹配),防止程序运行到一半崩溃,保证执行的原子性。
  • 构建 AST (抽象语法树):将纯文本转为机器能理解的逻辑树,这是后续生成字节码的必备前提。
  • 预分配内存 (Hoisting):在扫描阶段,引擎需要识别出所有的变量声明,从而在内存中提前开辟空间。

5.2 环境隔离:变量环境 vs. 词法环境

为了在兼容老旧 var 代码的同时,完美支持 ES6 的 let/const 块级作用域,V8 将执行上下文拆分为两个独立的存储区域:

变量环境 (Variable Environment)
  • 存放内容var 声明的变量、函数声明。
  • 设计目的 :维护传统的函数作用域
  • 底层行为 :在创建阶段,变量会被初始化为 undefined(产生变量提升现象)。
词法环境 (Lexical Environment)
  • 存放内容letconst 声明的变量、with 语句、try...catch
  • 设计目的 :支持块级作用域
  • 底层行为 :在创建阶段,变量仅被记录名称,不进行初始化(产生暂时性死区 TDZ)。

5.3 "物理隔离"带来的深远影响

这种设计决定了 JavaScript 在运行时的三个核心表现:

  1. 查找顺序

    当访问一个变量时,引擎优先查找当前上下文的词法环境 ,若无,再查找变量环境 ,最后顺着作用域链向上寻找。这保证了块级变量优先于函数级变量。

  2. 块级作用域的实现

    在执行过程中,每进入一个 {} 块,词法环境都会创建一个小型环境栈(Stack),并在退出块时将其销毁。这解决了 var 变量容易污染全局或循环体的问题。

  3. 暂时性死区 (TDZ)

    由于词法环境中的变量在声明前处于"未初始化"状态,任何提前访问都会触发错误。这迫使开发者养成"先声明后使用"的良好习惯。


🚀 核心总结表

特性 变量环境 (var) 词法环境 (let/const)
提升行为 提升声明并初始化为 undefined 提升声明但不初始化
作用域单位 函数 (Function Scope) 块 ({}) (Block Scope)
重复声明 允许 禁止
访问限制 自由访问(可能拿到 undefined) 严格限制(TDZ 报错)

💡 底层思考

这种"双环境"设计是现代 JS 引擎为了兼顾历史兼容性现代语言特性而做出的工程妥协,也是其性能与灵活性并存的秘密武器。

二、JavaScript 是单线程还是多线程?请问异步任务处理机制是怎么样的?分别说明浏览器与 Node 的事件循环机制

1. 核心定性:JavaScript 到底是不是单线程?

结论:JavaScript 语言执行是单线程的,但其运行宿主环境(浏览器/Node.js)是多线程的。

  • 为什么单线程? 主要为了避免复杂的 DOM 操作冲突(如:线程 A 删除节点,线程 B 修改节点)。
  • 如何处理异步? JS 引擎遇到异步任务(定时器、网络请求)时,会将其交给浏览器的其他线程(渲染线程、HTTP 线程、定时器线程、事件触发线程(处理点击、滚动事件))处理。处理完成后,回调函数会进入"任务队列"等待执行。

2. 异步任务全家桶 (全面清单)

异步任务根据执行优先级的不同,分为 宏任务 (Macrotask)微任务 (Microtask)

微任务 (Microtask) ------ 优先级最高

执行时机 :当前调用栈清空后,立即执行,且必须清空整个微任务队列,才会进行下一次渲染或执行宏任务。

  • Promise.then() / catch() / finally()
  • async / await (本质是 Promise 的语法糖)
  • process.nextTick (Node.js 特有,微任务中的"王者",优先级高于 Promise)
  • MutationObserver (浏览器端,监听 DOM 变化)
  • queueMicrotask() (手动开启微任务的官方 API)

宏任务 (Macrotask) ------ 优先级次之

执行时机 :由宿主环境发起。每轮事件循环只取出一个宏任务执行。

  • script (整体代码块,是第一个宏任务)
  • setTimeout / setInterval
  • setImmediate (Node.js 特有)
  • I/O 操作 (文件读写、网络请求回调、数据库操作)
  • UI Rendering (浏览器特有,每轮循环结束后视情况触发)
  • postMessage / MessageChannel

3. 事件循环 (Event Loop) 执行模型

浏览器的执行顺序

  1. 执行同步代码(属于第一个宏任务)。
  2. 同步代码执行完,检查并清空整个微任务队列
  3. (视情况) 进行 UI 渲染
  4. 从宏任务队列中取入一个任务执行。
  5. 回到步骤 2,循环往复。

Node.js 的执行顺序 (libuv)

Node.js 10+ 后与浏览器基本一致,但其底层分为 6 个阶段循环:

  1. timers :执行 setTimeout 等回调。
  2. pending callbacks:执行某些系统操作的回调。
  3. idle, prepare:内部使用。
  4. poll (轮询):处理 I/O 回调,这是最核心阶段。
  5. check :执行 setImmediate 的回调。
  6. close callbacks :执行关闭回调(如 socket.on('close'))。

4. 高频面试避坑指南 (Killer Points)

Q1:Promise 内部是异步的吗?

坑点new Promise((resolve) => { ... }) 括号里的代码是同步执行 的!只有 .then() 里面的回调才是异步微任务。

Q2:await 后面代码的执行顺序?

坑点await 这一行右边的表达式会立即执行。而 await 下方 的代码会被阻塞,并存入微任务队列(相当于 .then)。


💡 黄金总结

一个宏任务 → \rightarrow → 所有微任务 → \rightarrow → 渲染 → \rightarrow → 下一个宏任务。


5. 异步任务调度题

javascript 复制代码
// 作业题 console.log('stack [1]');
console.log('stack [1]'); 

setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);

const p = Promise.resolve();
for(let i = 0; i < 3; i++) {
    p.then(() => {
        setTimeout(() => {
            console.log('stack [4]')
            setTimeout(() => console.log("macro [5]"), 0);
            p.then(() => console.log('micro [6]'));
        }, 0);
        console.log("stack [7]");
    });
}

console.log("stack [8]"); 

5.1 第一轮:执行同步代码(第一个宏任务)

此时,代码从上到下扫一遍,同步代码直接进入 调用栈 执行,异步任务分发到各自队列。

  • 第 1 行:打印 stack [1]。
  • 第 2, 3 行:遇到 setTimeout,将 macro [2] 和 macro [3] 分发到 宏任务队列
  • 第 6-15 行:循环 3 次。p.then 是异步的,将三个 then 回调依次放入 微任务队列(标记为 micro A, B, C)。
  • 第 17 行:打印 stack [8]。

当前状态:

  • 控制台输出:stack [1] → \rightarrow → stack [8]
  • 微任务队列:[micro A, micro B, micro C]
  • 宏任务队列:[macro [2], macro [3]]

5.2 第二轮:清空微任务队列(核心环节)

步代码跑完,调用栈空了,事件循环立即去清空所有的微任务。

  • 执行 micro A:
    • 内部同步代码:打印 stack [7](循环第 1 次)。
    • 内部异步:遇到 setTimeout,将 stack [4] 的那个回调推入 宏任务队列。
  • 执行 micro B:
    • 内部同步代码:打印 stack [7](循环第 2 次)。
    • 内部异步:又将一个 stack [4] 推入 宏任务队列。
  • 执行 micro C:内部同步代码:
    • 打印 stack [7](循环第 3 次)。
    • 内部异步:再将一个 stack [4] 推入 宏任务队列。

当前状态:

  • 控制台输出:...stack [8] → \rightarrow → stack [7] → \rightarrow → stack [7] → \rightarrow → stack [7]
  • 微任务队列:空
  • 宏任务队列:[macro [2], macro [3], stack [4]-A, stack [4]-B, stack [4]-C]

5.3 第三轮:开始执行宏任务

微任务清空后,事件循环取宏任务队列中的 第一个 任务出来执行。

  • 执行 macro [2]:打印 macro [2]。

  • 执行 macro [3]:打印 macro [3]。

  • 执行第一个 stack [4]-A:

    • 同步代码:打印 stack [4]。

    • 异步嵌套1:setTimeout,将 macro [5] 推入宏任务队列末尾。

    • 异步嵌套2:p.then,将 micro [6] 推入 微任务队列。

注意! 宏任务执行完,会立即检查并清空微任务队列。所以此时会先打印 micro [6],再跑下一个宏任务。

以此类推,执行完 B 和 C 组。


5.4 🚀 最终输出顺序结果

为了方便你核对,最终的打印顺序如下:

  1. stack [1]

  2. stack [8]

  3. stack [7] (循环1次)

  4. stack [7] (循环2次)

  5. stack [7] (循环3次)

  6. macro [2]

  7. macro [3]

  8. stack [4] (A组)

  9. micro [6] (A组微任务优先执行)

  10. stack [4] (B组)

  11. micro [6] (B组微任务优先执行)

  12. stack [4] (C组)

  13. micro [6] (C组微任务优先执行)

  14. macro [5] (A组嵌套)

  15. macro [5] (B组嵌套)

  16. macro [5] (C组嵌套)

相关推荐
听风吹等浪起4 小时前
用Python和Pygame从零实现坦克大战
开发语言·python·pygame
灰色小旋风4 小时前
力扣合并K个升序链表C++
java·开发语言
_MyFavorite_4 小时前
JAVA重点基础、进阶知识及易错点总结(28)接口默认方法与静态方法
java·开发语言·windows
取码网4 小时前
最新在线留言板系统PHP源码
开发语言·php
环黄金线HHJX.4 小时前
龙虾钳足启发的AI集群语言交互新范式
开发语言·人工智能·算法·编辑器·交互
不写八个5 小时前
PHP教程006:ThinkPHP项目入门
开发语言·php
_MyFavorite_5 小时前
JAVA重点基础、进阶知识及易错点总结(31)设计模式基础(单例、工厂)
java·开发语言·设计模式
A.A呐5 小时前
【C++第二十三章】C++11
开发语言·c++
智算菩萨5 小时前
【Pygame】第8章 文字渲染与字体系统(支持中文字体)
开发语言·python·pygame
014-code5 小时前
Java SPI 实战:ServiceLoader 的正确打开方式(含类加载器坑)
java·开发语言