核心内容摘要
当“闭俗”小情侣误入玩咖辣妹的歌词训练营:一场关于灵魂尺度与韵律的极速超车
你的 JS 到底怎么跑起来的一文看懂 V8 从源码到机器码的“流水线”含图解写下console.log(hi)的那一刻CPU 其实完全看不懂。
真正让 JS “跑起来”的是JavaScript 引擎——尤其是 Chrome/Node.js 背后的V8。
这篇文章用一条清晰的流水线把 V8 的核心机制讲透Parse →AST →Ignition(字节码) →TurboFan(机器码) →Deopt(去优化回退) 。
先建立直觉V8 是一条“翻译加速”的流水线可以把 V8 想象成一个“会学习的翻译官”第一目标让代码尽快跑起来启动快第二目标把经常跑的代码越跑越快热点优化第三目标发现假设错了就回退重来去优化 Deopt接下来所有细节都围绕这三句话展开。
01为什么 CPU 才是最终执行者计算机的大脑——CPU中央处理器只认识机器语言指令集。
高级语言JavaScript, Python, Java人类易读抽象程度高。
机器语言二进制指令CPU 易读直接控制硬件。
JavaScript 引擎的本质就是一个超级翻译官。
它的核心职责就是解释和执行 JS 代码将其转换为 CPU 能看懂的机器指令。
所以CPU 是“执行者”V8 是“翻译官 加速器”。
再看一张更直观的图代码最终一定要落到 CPU 可执行的机器码上。
02JavaScript 引擎在浏览器里处在什么位置
1 常见的翻译官JS 引擎虽然今天的主角是 V8但在这个江湖里还有其他大佬SpiderMonkeyJS 之父 Brendan Eich 的作品Firefox 的心脏。
JavaScriptCore苹果 WebKit 内核的一部分Safari 的动力源。
Chakra微软 IE/Edge 时代的产物。
V8Google 的杰作它不仅驱动了 Chrome更是 Node.js 的核心。
浏览器内核并不是“只有渲染”它通常至少包含两大块渲染相关HTML/CSS 解析、布局、绘制脚本相关解析并执行 JavaScript以 WebKit 举例它可以拆成WebCore和JavaScriptCore两部分JS 引擎就是内核的一部分。
遇到script标签时WebCore 会暂停工作把控制权交给 JS 引擎。
这就是为什么我们常说 JS 会阻塞 DOM 渲染。
2.
为什么 V8 能从众神中脱颖而出V8 是用C编写的高性能开源引擎。
它之所以强主要归功于这几点JIT(Just-In-Time) 即时编译V8 抛弃了传统的“先转字节码再慢慢解释”或者“全量编译”的极端而是采用了混合模式。
它能在运行时将 JS 代码直接编译成机器码速度极快。
Node.js 的心脏V8 证明了 JS 不仅仅能跑在浏览器也能跑在服务器端实现了全栈的可能。
垃圾回收GCV8 拥有极其先进的分代式垃圾回收机制后续文章我们将专门拆解。
03V8 全流程从源码到机器码把 V8 的执行流程浓缩成 6 步会非常清晰Parse解析源码 → AST抽象语法树并采用Lazy Parsing函数即将执行时才完整解析Ignition解释器AST →字节码 Bytecode执行字节码先跑起来并收集运行信息类型、分支、调用频率…TurboFan优化编译器热点代码 →优化后的机器码Deopt去优化假设不成立常见是类型变化→ 回退到字节码机器码执行最终交给 CPU用一张图把这条流水线钉死在脑子里同时AST 长什么样大概是这种结构化树形表示04Parse 细节词法分析、语法分析与 AST很多人卡在“Parse 解析”这一步原因是概念名词多但直觉不够。
1 词法分析把代码拆成 token最小语法单元在生成树之前代码首先要被“打碎”。
这就是词法分析。
V8 会将代码拆解成一个个最小单元称为Tokens。
比如代码const name coderwhy会被拆解为const(Keyword 关键字)name(Identifier 标识符)(Operator 操作符)coderwhy(StringLiteral 字符串字面量)
2 语法分析把 token 重新组装成树AST拿到 Tokens 后V8 会依据语法规则将其组装成一棵树——AST抽象语法树。
AST 是前端工程化的基石Babel 转译、ESLint 检查、Vue 模板编译本质上都在操作 AST。
看看一段简单的代码生成的 AST 结构JavaScriptfunction sayHi(name) { console.log(Hi name) }对应的 AST 结构概览FunctionDeclaration(函数声明: sayHi)Identifier(参数: name)BlockStatement(函数体)ExpressionStatement(表达式)CallExpression(调用 console.log)05为什么要保留“字节码”这一层直觉上会觉得少一层转换就更快那为什么不直接 AST → 机器码因为工程里真正的目标不是“某一步最快”而是“整体更快、更稳、更可控”。
保留字节码主要带来跨平台字节码不绑定某一种 CPU 指令集优化更聪明先跑字节码收集运行数据再决定怎么生成更优机器码更安全、更可控更容易做隔离、策略、内存管理更容易调试断点/单步在字节码层更容易实现配合这张图理解会很顺06架构拆解Parse / Ignition / TurboFan 各做什么用“岗位职责”来记Parse把 JS 代码变成 AST解释器不直接认识 JS 源码Ignition把 AST 变成字节码并执行同时收集 TurboFan 需要的运行信息比如类型信息TurboFan把热点字节码编译成更快的机器码并持续迭代优化这里有一个非常关键的运行规律热点函数会被优化但类型变化等情况会触发去优化回退。
07预解析 vs 全量解析Lazy Parsing 为什么能让启动更快V8 并不会“上来就把一切都解析得巨细无遗”它会做取舍
1 预解析Pre-parsing目标快速扫描提取结构信息变量/函数声明等特点不深挖函数体内部逻辑 →更快
2 全量解析Full parsing目标把函数体、表达式、语句细节全部建出来特点AST 更完整 → 便于后续生成字节码与优化因此“函数没执行会不会生成 AST”更准确的回答是会生成一个简化的结构架子预解析真要执行之前会补齐为完整 AST全量解析08走一遍官方图token、AST、字节码到底怎么来的先准备一段模板代码name XiaoWu console.log(name) function sayHi(name) { console.log(Hi name) } sayHi(name)
1 官方流程图从输入到字节码这张图非常经典建议收藏按图理解就是Scanner扫描字符流 → 生成 tokensPreParser做预解析快速判断结构Parser构建 ASTBytecodeAST → 字节码
2 token 长什么样词法分析结果下面是典型 token 形态摘取关键类型方便理解Token(typeKeyword, valueconst) // 关键字 Token(typeIdentifier, valuename) // 标识符 Token(typeOperator, value) // 运算符 Token(typeStringLiteral, valuecoderwhy and XiaoYu) // 字符串字面量 Token(typePunctuation, value;) // 标点符号 Token(typeIdentifier, valueconsole) Token(typePunctuation, value.) Token(typeIdentifier, valuelog) Token(typePunctuation, value() Token(typeIdentifier, valuename) Token(typePunctuation, value)) Token(typePunctuation, value;)
3 语法分析预解析如何参与这张图专门解释“预解析/解析”的关系09热点优化与去优化为什么“有时突然变慢”V8 会把被频繁执行的函数标记为热点函数然后交给 TurboFan 编译为更快的机器码。
但注意优化是有前提假设的。
最常见的假设就是“类型稳定”。
来看这个例子function sum (num1,num
{ return num1 num2 } // 多次调用 - 可能成为热点函数 - 被优化 sum(20,
sum(20,
// 类型突然变化 - 之前的机器码假设不成立 - 去优化回退 sum(xiaoyu,coderwhy)发生了什么前两次传入number优化器可能会假设“这里一直是 number 加法”第三次突然变成string拼接机器码可能无法正确处理 →回退到字节码重新收集信息再决定是否重新优化这就是性能“抖一下”的根源之一Deopt去优化。
10字节码与机器码了解即可JIT 到底做了什么机器码的生成通常依赖JITJust-In-Time Compilation即时编译把字节码转换成本地机器码把结果缓存起来后续执行直接复用缓存的机器码更快TurboFan 作为优化编译器会基于 IR中间表示做多层优化类型、内联、控制流等同时字节码到机器码的过程中会存在不同优化策略这里还有两张配图保持原样保留结尾把知识用起来看到这里你是否对那个黑盒子一样的浏览器有了新的认识我们简单回顾一下 V8 的奇幻旅程Scanner Parser把代码拆解成 Tokens再组装成 AST注意预解析优化。
Ignition把 AST 翻译成字节码边解释边执行。
TurboFan在后台偷偷观察把热点代码编译成机器码但一旦类型变化就会被打回原形。