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

前言

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

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

1、解释器和编译器

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

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

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

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

大致流程如下:

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

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

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

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

将源代码转换为抽象语法树(AST) ,并生成执行上下文 。执行上下文在 【JavaScript专栏】前面两篇文中有详细的讲解,

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

什么是 AST

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

以下这段代码解析成 AST:

JS 复制代码
var myName = "zhihu"
function foo(){
  return 23;
}
myName = "juejin"
foo()

这段代码在 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,指的是语法上不可能再分的、最小的单个字符或字符串。

可以看出,通过 var myName = "juejin" 简单地定义了一个变量,其中关键字"var"、标识符"myName" 、赋值运算符"="、字符串"juejin"四个都是 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 的工作原理,如有不对,欢迎指正。🤝

相关推荐
TU^几秒前
C语言习题~day16
c语言·前端·算法
一颗花生米。3 小时前
深入理解JavaScript 的原型继承
java·开发语言·javascript·原型模式
学习使我快乐013 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19953 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
勿语&4 小时前
Element-UI Plus 暗黑主题切换及自定义主题色
开发语言·javascript·ui
黄尚圈圈4 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水5 小时前
简洁之道 - React Hook Form
前端
正小安7 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch9 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j