文章目录
- 一、有了解过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) 执行模型)
-
- **浏览器的执行顺序**
- [**Node.js 的执行顺序 (libuv)**](#Node.js 的执行顺序 (libuv))
- [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 的工作流程:
- 快速响应 :代码加载时,解释器首先介入,快速开始执行,让用户感知不到延迟。
- 热点探测 :在运行过程中,引擎会监控哪些代码块被频繁执行(称为 Hot Spot / 热点代码)。
- 即时编译 :JIT 编译器 将这些热点代码直接编译为高效的机器码。
- 替换执行:当再次遇到相同代码时,直接调用编译好的机器码,跳过解释步骤。
性能 ≈ 解释器的响应速度 + 编译器的执行效率 性能 \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 | Chrome, Node.js, Electron, Edge | |
| SpiderMonkey | Mozilla | Firefox |
| JavaScriptCore | Apple | Safari, iOS 全线应用 |
| Chakra | Microsoft | 早期 Edge, IE (已逐步退出舞台) |
2.3 引擎内部是如何工作的? (以 V8 为例)
现代引擎的工作流程并不是简单的"翻译",而是一个复杂的流水线:
- 解析 (Parsing) :
- 引擎将源码拆解为 Tokens (记号)。
- 生成 AST (Abstract Syntax Tree, 抽象语法树),这是代码的结构化表示。
- 解释 (Interpretation) :
- 解释器(V8 中的 Ignition )将 AST 转换为中间形态的 字节码 (Bytecode) 并开始执行。这一步保证了代码能以最快速度启动。
- 编译与优化 (JIT Compilation) :
- 引擎会监控运行状态。如果某段代码运行非常频繁(Hot Spot / 热点代码 ),编译器(V8 中的 TurboFan )会将其直接编译为高性能的机器码。
- 垃圾回收 (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):
- 解析 (Parsing) :
- 解析 HTML → 生成 DOM 树。
- 解析 CSS → 生成 CSSOM 树。
- 构建渲染树 (Render Tree) :
- 将 DOM 与 CSSOM 合并。引擎会过滤掉不需要显示的元素(如
display: none)。
- 将 DOM 与 CSSOM 合并。引擎会过滤掉不需要显示的元素(如
- 布局 (Layout / Reflow) :
- 计算每个节点在屏幕上的确切几何位置(高、宽、坐标)。
- 绘制 (Painting) :
- 将渲染树中的每个节点转换成屏幕上的实际像素点。
- 合成 (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)
- 存放内容 :
let、const声明的变量、with语句、try...catch。 - 设计目的 :支持块级作用域。
- 底层行为 :在创建阶段,变量仅被记录名称,不进行初始化(产生暂时性死区 TDZ)。
5.3 "物理隔离"带来的深远影响
这种设计决定了 JavaScript 在运行时的三个核心表现:
-
查找顺序 :
当访问一个变量时,引擎优先查找当前上下文的词法环境 ,若无,再查找变量环境 ,最后顺着作用域链向上寻找。这保证了块级变量优先于函数级变量。
-
块级作用域的实现 :
在执行过程中,每进入一个
{}块,词法环境都会创建一个小型环境栈(Stack),并在退出块时将其销毁。这解决了var变量容易污染全局或循环体的问题。 -
暂时性死区 (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/setIntervalsetImmediate(Node.js 特有)I/O操作 (文件读写、网络请求回调、数据库操作)UI Rendering(浏览器特有,每轮循环结束后视情况触发)postMessage/MessageChannel
3. 事件循环 (Event Loop) 执行模型
浏览器的执行顺序
- 执行同步代码(属于第一个宏任务)。
- 同步代码执行完,检查并清空整个微任务队列。
- (视情况) 进行 UI 渲染。
- 从宏任务队列中取入一个任务执行。
- 回到步骤 2,循环往复。
Node.js 的执行顺序 (libuv)
Node.js 10+ 后与浏览器基本一致,但其底层分为 6 个阶段循环:
- timers :执行
setTimeout等回调。 - pending callbacks:执行某些系统操作的回调。
- idle, prepare:内部使用。
- poll (轮询):处理 I/O 回调,这是最核心阶段。
- check :执行
setImmediate的回调。 - 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 🚀 最终输出顺序结果
为了方便你核对,最终的打印顺序如下:
-
stack [1]
-
stack [8]
-
stack [7] (循环1次)
-
stack [7] (循环2次)
-
stack [7] (循环3次)
-
macro [2]
-
macro [3]
-
stack [4] (A组)
-
micro [6] (A组微任务优先执行)
-
stack [4] (B组)
-
micro [6] (B组微任务优先执行)
-
stack [4] (C组)
-
micro [6] (C组微任务优先执行)
-
macro [5] (A组嵌套)
-
macro [5] (B组嵌套)
-
macro [5] (C组嵌套)