V8 是如何执行一段JavaScript代码的?
前言
前端工具和框架更新速度非常快,而且还不断有新的出现。要想追赶上前端工具和框架的更新速度,就需要抓住那些本质的知识,才能更加轻松地理解这些上层应用。
本文站在 JavaScript 引擎 V8 的视角,来分析 JavaScript 代码是如何被执行的。能帮助我们从底层了解 JavaScript,也能深入理解语言转换器 Babel、语法检查工具 ESLint等 一些底层实现机制;能让我们将很多模糊的概念关联起来,使其变得更加清楚,从而拓宽视野,上升到更高的层次。
1、解释器和编译器
之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。按语言的执行流程,可以把语言划分为编译型语言和解释型语言。
编译型语言
在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。如 C/C++
解释型语言
在每次运行时都需要通过解释器对程序进行动态解释和执行。如 JavaScript
编译器和解释器是这样转化代码:
大致流程如下:
- 在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。
- 在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。
2、V8 如何执行一段JavaScript代码
V8 在执行过程中既有解释器 (Ignition),又有编译器 (TurboFan),那么它们是如何配合去执行一段 JavaScript 代码的呢?
2.1 生成抽象语法树(AST)和 执行上下文
什么是 AST 以及 AST 的生成过程是怎样的呢?
什么是 AST
高级语言是开发者可以理解的语言,但是让编译器或者解释器来理解就非常困难了。对于编译器或者解释器来说,它们可以理解的就是 AST 了。所以无论使用的是解释型语言还是编译型语言,在编译过程中,它们都会生成一个 AST。这和渲染引擎将 HTML 文件转换为计算机可以理解的 DOM 树这种情况类似。
以下这段代码解析成 AST:
这段代码在 JavaScript-AST (将JavaScript生成AST的模拟器)解析后的 AST 如下:
可以看出,AST 的结构和代码的结构非常相似,可以把 AST 看成代码的结构化的表示。
AST
是非常重要的一种数据结构,在很多项目中有着广泛的应用。其中最著名的一个项目是 Babel
。Babel 是一个被广泛使用的代码转码器,Babel 的工作原理就是先将 ES6 源码转换为 AST,然后再将 ES6 语法的 AST 转换为 ES5 语法的 AST,最后利用 ES5 的 AST 生成 JavaScript 源代码。
除了 Babel 外,还有 ESLint
也使用 AST。ESLint 是一个用来检查 JavaScript 编写规范的插件,其检测流程也是需要将源码转换为 AST,然后再利用 AST 来检查代码规范化的问题。
AST 是怎么生成的
生成 AST 需要经过两个阶段:
1、第一阶段是分词(tokenize
),又称为词法分析,其作用是将一行行的源码拆解成一个个 token。所谓 token,指的是语法上不可能再分的、最小的单个字符或字符串。

2、第二阶段是解析(parse
),又称为语法分析,其作用是将上一步生成的 token 数据,根据语法规则转为 AST。
这就是 AST 的生成过程,先分词,再解析。(先词法分析,在语法分析)
2.2 生成字节码
有了 AST 和执行上下文后,解释器(Ignition)会根据 AST 生成字节码,并解释执行字节码。
其实一开始 V8 并没有字节码,而是直接将 AST 转换为机器码,由于执行机器码的效率是非常高效的,所以这种方式在发布后的一段时间内运行效果是非常好的。但是随着 Chrome 在手机上的广泛普及,内存占用问题也暴露出来了,因为 V8 需要消耗大量的内存来存放转换后的机器码。为了解决内存占用问题,V8 引入字节码。
为什么引入字节码就能解决内存占用问题呢?
字节码就是介于 AST 和机器码之间的一种代码。字节码需要通过解释器将其转换为机器码后才能执行。
同一行代码,转换为 字节码 和 机器码,如下图:
可以看出,机器码所占用的空间远远超过了字节码,所以使用字节码可以减少系统的内存使用。
2.3 执行代码
如果是第一次执行的字节码,解释器 Ignition 会逐条解释执行。
在 解释器(Ignition) 执行字节码的过程中,如果发现有 热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,编译器 (TurboFan) 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
这种字节码配合解释器和编译器的技术,就是 即时编译(JIT)
: 指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
JIT 的工作过程:
看看 V8 的解释器和编译器的取名:解释器 Ignition 是点火器的意思,编译器 TurboFan 是涡轮增压的意思,寓意着代码启动时通过点火器发动,一旦启动,涡轮增压介入,其执行效率随着执行时间越来越高效率,因为热点代码都被编译器 TurboFan 转换了机器码,直接执行机器码就省去了字节码“翻译”为机器码的过程。
总结
- 编译器和解释器:
- 编译器 :把程序编译成机器码。
- 解释器:负责把 AST 转换成 字节码,并解释执行。
- V8 执行代码流程:
- 经过分词、解析,将源代码转化成 AST 和 执行上下文。
- 基于 AST,解释器把 AST 生成 字节码。
- 解释器逐条解释执行字节码,通过编译器来优化编译字节码。
- 抽象语法数(AST) 的生成:
- 分词:将源码拆解成一个个 token。
- 解析:将生成的 token 数据,根据语法规则转为 AST。
- JIT 技术: 遇到热点代码,编译器将其编译成机器码保存,下次直接使用
本文只是浅浅的记录 V8 的工作原理,如有不对,欢迎指正。🤝
转载自:https://juejin.cn/post/7277543238276759612