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

 更新时间:2026年06月06日 09:55:04   作者:Hello--_--World  
了解JavaScript引擎与运行机制,包括解释型与编译型语言的区别,JIT技术如何提升性能,以及JavaScript的单线程特性与异步任务处理机制,通过解析、解释、监控与优化等步骤,现代JavaScript引擎如如V8,实现了高效的代码执行

一、有了解过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, GoPython, PHPJavaScript (V8), Java

进阶知识:

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

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

2.1 什么是 JavaScript 引擎?

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

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

2.2 主流引擎概览

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

引擎名称开发者主要应用环境
V8GoogleChrome, Node.js, Electron, Edge
SpiderMonkeyMozillaFirefox
JavaScriptCoreAppleSafari, iOS 全线应用
ChakraMicrosoft早期 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 三足鼎立:主流引擎分布

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

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

3.3 渲染流水线 (Rendering Pipeline)

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

解析 (Parsing)

  • 解析 HTML → 生成 DOM 树
  • 解析 CSS → 生成 CSSOM 树

构建渲染树 (Render Tree)

  • 将 DOM 与 CSSOM 合并。引擎会过滤掉不需要显示的元素(如 display: none)。

布局 (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 的执行流水线

状态处理过程产物特点
初始源码读取字符串人类可读
分析ParsingAST 树逻辑结构化
启动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 在运行时的三个核心表现:

查找顺序

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

块级作用域的实现

在执行过程中,每进入一个 {} 块,词法环境都会创建一个小型环境栈(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 / 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. 异步任务调度题

// 作业题 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组嵌套)

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • JS获取当前时间实例代码(年月日时分秒)

    JS获取当前时间实例代码(年月日时分秒)

    在javascript中,可以使用Date对象中的Date()方法来获取当前时间,下面这篇文章主要给大家介绍了关于JS获取当前时间(年月日时分秒)的相关资料,需要的朋友可以参考下
    2022-09-09
  • 深入探讨前端性能优化的核心策略和实战技巧指南

    深入探讨前端性能优化的核心策略和实战技巧指南

    性能优化是前端开发中至关重要的一环,优秀的性能不仅提升用户体验,还能提高转化率、降低跳出率,并改善 SEO 排名,本文将深入探讨前端性能优化的核心策略和实战技巧,希望对大家有所帮助
    2026-04-04
  • 微信小程序-可移动菜单的实现过程详解

    微信小程序-可移动菜单的实现过程详解

    这篇文章主要介绍了微信小程序-可移动菜单的实现过程详解,我们可以经常看到手机app里有的菜单栏是悬浮在首页的,用户可以拖动和点击菜单栏进行交互,今天就教大家利用小程序的控件,,需要的朋友可以参考下
    2019-06-06
  • JavaScript显示当然日期和时间即年月日星期和时间

    JavaScript显示当然日期和时间即年月日星期和时间

    使用js显示当然日期和时间在网页中很是常见,方法有很多,不过多说大同小异,下面有个不错的示例,需要的朋友可以感受下
    2013-10-10
  • js项目中添加ts支持实现示例详解

    js项目中添加ts支持实现示例详解

    这篇文章主要为大家介绍了如何在js项目中添加ts支持实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • JavaScript 管道运算符及工作原理

    JavaScript 管道运算符及工作原理

    这篇文章主要介绍了JavaScript 管道运算符,管道运算符为我们的代码添加了大量上下文,并简化了操作,以便以后可以扩展它们,本文结合示例代码给大家介绍的非常详细,需要的朋友可以参考下
    2023-05-05
  • js+css实现飞机大战游戏

    js+css实现飞机大战游戏

    这篇文章主要为大家详细介绍了js+css实现飞机大战游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-05-05
  • uniapp手写滚动选择器的完整代码(时间选择器)

    uniapp手写滚动选择器的完整代码(时间选择器)

    这篇文章主要介绍了uniapp手写滚动选择器的完整代码(时间选择器),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2024-07-07
  • js实现的类似于asp数据字典的数据类型代码实例

    js实现的类似于asp数据字典的数据类型代码实例

    这篇文章主要介绍了js实现的类似于asp数据字典的数据类型代码实例,即js实现的字典数据类型,需要的朋友可以参考下
    2014-09-09
  • JavaScript中的Reflect对象详解(ES6新特性)

    JavaScript中的Reflect对象详解(ES6新特性)

    这篇文章主要介绍了JavaScript中的Reflect对象(ES6新特性)的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-07-07

最新评论