likes
comments
collection
share

V8 是如何执行一段JavaScript代码的?

作者站长头像
站长
· 阅读数 90

前言

前端工具和框架更新速度非常快,而且还不断有新的出现。要想追赶上前端工具和框架的更新速度,就需要抓住那些本质的知识,才能更加轻松地理解这些上层应用。

本文站在 JavaScript 引擎 V8 的视角,来分析 JavaScript 代码是如何被执行的。能帮助我们从底层了解 JavaScript,也能深入理解语言转换器 Babel、语法检查工具 ESLint等 一些底层实现机制;能让我们将很多模糊的概念关联起来,使其变得更加清楚,从而拓宽视野,上升到更高的层次。

1、解释器和编译器

之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将我们所写的代码“翻译”成机器能读懂的机器语言。按语言的执行流程,可以把语言划分为编译型语言和解释型语言。

编译型语言 在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。如 C/C++

解释型语言 在每次运行时都需要通过解释器对程序进行动态解释和执行。如 JavaScript

编译器和解释器是这样转化代码:

V8 是如何执行一段JavaScript代码的?

大致流程如下:

  1. 在编译型语言的编译过程中,编译器首先会依次对源代码进行词法分析、语法分析,生成抽象语法树(AST),然后是优化代码,最后再生成处理器能够理解的机器码。
  2. 在解释型语言的解释过程中,同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后再根据字节码来执行程序、输出结果。

2、V8 如何执行一段JavaScript代码

V8 是如何执行一段JavaScript代码的?

V8 在执行过程中既有解释器 (Ignition),又有编译器 (TurboFan),那么它们是如何配合去执行一段 JavaScript 代码的呢?

2.1 生成抽象语法树(AST)和 执行上下文

什么是 AST 以及 AST 的生成过程是怎样的呢?

什么是 AST

高级语言是开发者可以理解的语言,但是让编译器或者解释器来理解就非常困难了。对于编译器或者解释器来说,它们可以理解的就是 AST 了。所以无论使用的是解释型语言还是编译型语言,在编译过程中,它们都会生成一个 AST。这和渲染引擎将 HTML 文件转换为计算机可以理解的 DOM 树这种情况类似。

以下这段代码解析成 AST:

这段代码在 JavaScript-AST (将JavaScript生成AST的模拟器)解析后的 AST 如下:

V8 是如何执行一段JavaScript代码的?

可以看出,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,指的是语法上不可能再分的、最小的单个字符或字符串。

V8 是如何执行一段JavaScript代码的?

2、第二阶段是解析parse),又称为语法分析,其作用是将上一步生成的 token 数据,根据语法规则转为 AST。

V8 是如何执行一段JavaScript代码的?

这就是 AST 的生成过程,先分词,再解析。(先词法分析,在语法分析)

2.2 生成字节码

有了 AST 和执行上下文后,解释器(Ignition)会根据 AST 生成字节码,并解释执行字节码。

其实一开始 V8 并没有字节码,而是直接将 AST 转换为机器码,由于执行机器码的效率是非常高效的,所以这种方式在发布后的一段时间内运行效果是非常好的。但是随着 Chrome 在手机上的广泛普及,内存占用问题也暴露出来了,因为 V8 需要消耗大量的内存来存放转换后的机器码。为了解决内存占用问题,V8 引入字节码。

为什么引入字节码就能解决内存占用问题呢?

字节码就是介于 AST 和机器码之间的一种代码。字节码需要通过解释器将其转换为机器码后才能执行。

同一行代码,转换为 字节码 和 机器码,如下图:

V8 是如何执行一段JavaScript代码的?

可以看出,机器码所占用的空间远远超过了字节码,所以使用字节码可以减少系统的内存使用。

2.3 执行代码

如果是第一次执行的字节码,解释器 Ignition 会逐条解释执行。

在 解释器(Ignition) 执行字节码的过程中,如果发现有 热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,编译器 (TurboFan) 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。

这种字节码配合解释器和编译器的技术,就是 即时编译(JIT): 指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。

JIT 的工作过程:

V8 是如何执行一段JavaScript代码的?

看看 V8 的解释器和编译器的取名:解释器 Ignition 是点火器的意思,编译器 TurboFan 是涡轮增压的意思,寓意着代码启动时通过点火器发动,一旦启动,涡轮增压介入,其执行效率随着执行时间越来越高效率,因为热点代码都被编译器 TurboFan 转换了机器码,直接执行机器码就省去了字节码“翻译”为机器码的过程。

总结

  • 编译器和解释器:
    • 编译器 :把程序编译成机器码。
    • 解释器:负责把 AST 转换成 字节码,并解释执行。
  • V8 执行代码流程:
    • 经过分词、解析,将源代码转化成 AST 和 执行上下文。
    • 基于 AST,解释器把 AST 生成 字节码。
    • 解释器逐条解释执行字节码,通过编译器来优化编译字节码。
  • 抽象语法数(AST) 的生成:
    • 分词:将源码拆解成一个个 token。
    • 解析:将生成的 token 数据,根据语法规则转为 AST。
  • JIT 技术: 遇到热点代码,编译器将其编译成机器码保存,下次直接使用

本文只是浅浅的记录 V8 的工作原理,如有不对,欢迎指正。🤝

转载自:https://juejin.cn/post/7277543238276759612
评论
请登录