3.JS高级-V8引擎的运行原理

5,528 阅读30分钟

该系列文章连载于公众号coderwhy和掘金XiaoYu2002中

脉络探索

  • 在上一章节中,我们讲解了当在浏览器中输入URL后,浏览器是如何解析页面的
    • 但目前所讲到的解析页面只有HTML+CSS的处理,我们并没有看到JS的部分。但JS肯定是在其中有重度参与进来的,只是这会是一种什么形式呢?
    • 这就是我们本章节所需要探讨的部分,JS是通过一个叫做JavaScript引擎的东西才进行参与其中的
  • 在本章节中,我们会详细探索什么是JavaScript引擎?这引擎和浏览器内核的关系是怎么样的?最主要的JavaScript引擎都有哪些?引擎是如何工作的?又是如何对JS进行处理的?
    • 只有了解了引擎和浏览器之间的联系,我们才能知道JS是如何通过引擎参与进浏览器之中的
    • 而只有清楚了引擎如何工作的,才能知晓如何对JS进行处理的(V8引擎的转化代码过程)
  • 那么,就让我们马上开始吧!也许先对什么是JavaScript引擎有一个第一印象会是不错的开局

一、认识JavaScript引擎

1.1. 什么是JavaScript引擎

JavaScript引擎是一个解释和执行JavaScript代码的程序,负责将JavaScript代码转换成可执行的机器代码。这个过程包括解析代码、进行优化,并最终执行。通过它,JS能在浏览器或服务器环境中运行,处理从简单的脚本到复杂应用的各种计算任务

当我们编写JavaScript代码时,它实际上是一种高级语言,这种语言并不是机器语言。

  • 高级语言是设计给开发人员使用的,它包括了更多的抽象和可读性。
  • 但是,计算机的CPU只能理解特定的机器语言,它不理解JavaScript语言。
  • 这意味着,在计算机上执行JavaScript代码之前,必须将其转换为机器语言。

这就是JavaScript引擎的作用:

  • 事实上我们编写的JavaScript无论交给浏览器还是Node执行,最后都是需要被CPU执行的;
  • 但是CPU只认识自己的指令集,实际上是机器语言,才能被CPU所执行;
  • 所以我们需要JavaScript引擎帮助我们将JavaScript代码翻译成CPU指令来执行;

1.1.1. CPU对于JS引擎的意义

CPU(中央处理器)是计算机的主要硬件之一,负责解释计算机程序中的指令以及处理计算机软件中的数据。它执行程序的基本算术、逻辑、控制和输入/输出(I/O)操作,指定这些操作的基本指令系统。CPU是计算机的大脑,负责执行操作系统和应用程序的命令。现代CPU通常包括多个处理核心,这使它们能够同时处理多个任务

  • CPU是真正的代码执行者,而JS引擎则相当于一个翻译官
  • 前端通常很少理解到硬件层,有兴趣的可以通过学习408(计算机四件套)来进一步掌握,接下来就让我们来看下这个步骤
    • CPU又叫做中央处理器 执行的是机器语言,也就我们所说的底层编码语言,由一系列的二进制代码组成,能够直接控制硬件操作,也是真正的代码执行源头

代码执行者CPU

图3-1 代码执行者CPU

  • 这样如图3-1,通过引擎的转换,JavaScript 代码最终能在硬件上运行,实现各种功能和任务

1.1.2. 常见的JavaScript引擎

比较常见的JavaScript引擎有哪些呢?

  • SpiderMonkey:第一款JavaScript引擎,由Brendan Eich开发(也就是JavaScript作者);
  • Chakra:微软开发,用于IT浏览器;
  • JavaScriptCore:WebKit中的JavaScript引擎,Apple公司开发;
  • V8:Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出;
    • 这就是为什么早年谷歌浏览器的浏览速度会快于其他浏览器的原因,这都得益于V8引擎的强大,但到如今,很多的浏览器也都开始使用起谷歌的这个引擎了(因为是开源的)。所以这些套用谷歌v8引擎的浏览器有时候也会被人叫做套壳浏览器
  • 等等…

1.2. 浏览器内核与JS引擎关系

  • 如图3-2,我们先以WebKit为例,WebKit事实上由两部分组成的:
    • WebCore:负责HTML解析、布局、渲染等等相关的工作
    • JavaScriptCore:解析、执行JavaScript代码

组成WebKit的两部分

图3-2 组成WebKit的两部分

  • 所以JavaScript引擎是浏览器内核的一个组成部分。内核负责整体的页面布局和资源加载,而JavaScript引擎专注于处理JavaScript语言
    • 当网页被加载时,浏览器内核首先解析HTML和CSS,构建DOM树和渲染树,也是我们讲过的重点内容。在此过程中,当内核遇到<script>标签时,它会调用JavaScript引擎来处理脚本,执行可能会影响DOM的操作(也就是我们上一章节所说的重绘与重排)。而JS代码就是在这时候参与进来的
  • 既然引擎这么重要,我们就需要单独拿出来进行一个讲解了,可JavaScript引擎其实不少,我们要讲解哪一个呢?
    • 我们最终选择的是V8引擎,而选择的原因会是以下的几点主要因素:
      1. V8引擎是Google开发的JavaScript引擎,广泛应用于Chrome浏览器和Node.js环境中。这意味着V8不仅支持客户端的JS执行,也支持服务器端的JS应用,成为我们学习JS引擎工作原理的理想选择
      2. 为速度优化而设计的,提供快速的JS执行速度,也就是前面所说谷歌浏览器运行加载明显更快的原因。V8引擎实现了即时编译(JIT),将JS代码直接编译成机器码,而非先转换为字节码。少了一层转换消耗,明显的提升运行效率
      3. V8持续在性能优化、内存管理和现代JavaScript特性支持方面进行创新。例如,它的垃圾回收机制和优化编译器都是其它引擎所参考的对象,而这也是我们所需要学习的重点,作为领头羊的V8是其不二选择
      4. 它是开源的项目,而开源精神是计算机能够蓬勃发展的核心,有数不尽的开发者围着这达到百万行的V8引擎项目,形成了一个活跃的社区,有大量的文档和社区支持可以帮助我们学习和解决在使用V8时遇到的问题
      5. 并且V8是Node.js的核心,理解V8的工作原理,对于我们后期理解Node也很有帮助,距离实现全栈开发也会更近一步

二、V8引擎的原理

  • 我们来看一下官方对V8引擎的定义:

    • V8是用C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等
    • 它实现ECMAScriptWebAssembly,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux系统上运行(兼容性很好)
    • V8可以独立运行(很少这么做),也可以嵌入到任何C ++应用程序中(比如Node.js)
  • WebAssembly (通常缩写为Wasm) 也是一种为高性能网络应用设计的编程语言,和JS是高度集成的,在V8中的作用主要是允许更多的复杂和计算密集型任务可以在浏览器中高效执行。不过我们暂时不需要太过于关系这点

    • 毕竟V8引擎主要目标是提高JavaScript代码的性能和执行速度
  • 还记得上面我们画的,JS代码到CPU执行的过程吗?

    • 在中间的JS引擎都是做了什么,我们将图3-3的V8引擎所做的流程铺开来看看

MDN文档对call的定义

图3-3 V8引擎工作图

2.1. V8引擎处理流程

  1. 解析 (Parse): 输入的JS源代码首先被解析器(Parse)处理,解析成抽象语法树(AST)。这一步是编程语言的常规处理过程,目的是将源代码转换成结构化的、便于进一步处理的内部表示形式
  2. 生成字节码 (Bytecode): AST 接下来被 Ignition(V8的解释器)处理,转换成字节码。字节码是介于源代码和机器代码之间的一种低级语言,比源代码更接近机器语言,但不是特定于任何单一的硬件
  3. 执行字节码 (Execution): 字节码随后被执行。在这一阶段,代码运行并进行实际的计算
  4. 优化 (Optimization): TurboFan(V8的优化编译器)根据运行时的数据对字节码进行优化,生成优化的机器代码。优化的目的是提高代码的执行效率,通过诸如内联函数、消除冗余代码等技术来增强性能
  5. 去优化 (Deoptimization): 如果优化的假设在实际运行中不成立,优化的代码需要被去优化,回退到字节码执行,这一过程称为去优化
  6. 生成机器代码 (Machine Code): 优化后的代码被转换为机器代码,即直接由计算机硬件执行的代码。这一步骤确保代码运行得尽可能快
组件中文意思作用描述
Parse解析器负责将JavaScript代码解析成抽象语法树(AST)。这是代码执行的第一步,为后续的编译和执行做准备。
Ignition解释器将AST转换为字节码并解释执行。Ignition作为中间步骤,优化启动速度和执行效率,同时为TurboFan提供运行时数据。
TurboFan优化编译器用于将热点代码(经常执行的代码)从字节码编译成优化的机器代码,以提高执行效率。它会根据代码的运行情况动态优化,以期达到更好的性能。

2.1.1. JS源代码到字节码之前发生了什么

2.1.1.1. 词法分析和语法分析
  • 在讨论发生了什么之前,我们需要先铺垫一点,什么是词法与语法?这有助于让我们了解Parse解析这一过程是怎么样的

    • 词法分析:编译过程中的第一阶段,主要任务是读取源代码的字符序列,将它们组合成有意义的“词素”(lexeme),并产生相应的记号(token)。记号通常包括标识符、关键字、运算符等
    • 语法分析:编译过程中的第二阶段,紧随词法分析之后。它使用词法分析输出的记号序列,根据预定义的语法规则(通常以文法形式给出),构建一个称为“语法树”(syntax tree)的结构
    • 词法与语法的区别:词法分析是将代码分割成基础块并标记它们,而语法分析则是将这些块组装成结构化的表达形式
  • 所以这就很好理解了,我们只是将原有的代码拆掉,然后装成了一种更好读取的结构化形态而已,这个形态就是我们要的AST语法树

    • 在词法分析中,拆得有多细?我想对于刚接触的同学来说,可能会是一个疑惑的点
      1. 在这里,会直接拆到拆不下去为止,是程序语法中的最小单元(也叫词法单元)
      2. 变量名、关键字、运算符等都是词法单元
    • 而在语法分析中,难道就直接进行转化为AST语法树了吗?
      1. 其实也不是,V8引擎在这个过程中还是会检查一下代码是否符合JavaScript语言规范
  • 最后,我们可以利用一个小技巧来方便我们记忆顺序,以免把两者的功能弄混

    • 词是单词,语是语句。语句是由单词组成,而原本是完整的JS代码,我们先拆开后合起来,拆开了自然就是单词,合起来的自然就是语句。所以先词后语,编译的第一阶段和第二阶段就这样记住了

2.1.2. 为什么不直接转化为机器语言

  • 在这个JS引擎的处理流程中,我们发现了并不是一蹴而就的将JS代码直接转化为机器语言,在这种中间还有一层字节码

    • 如果说转化的层数越少,就越高效,那此时的转化字节码为什么会被V8引擎所保留?

    • 这我们就需要去从V8处理流程中寻找答案了

      1. 首先在生成字节码的时候,我们说不特定于任何单一硬件。这体现了其中的一个优势:跨平台兼容,因为无需针对每种硬件配置重新编译代码,就可以适应不同设备和操作系统

      2. 并且如图3-4在处理流程第四步中有进行优化,减轻了多一层转化流程所造成的性能影响,第五步则是对优化好的代码进行检测,看是否成立,不成立就会回退反复进行,直到成立后才生成机器代码

        MDN文档对call的定义

        图3-4 字节码优化为机械代码过程

    • 从流程中我们能看出这些优势,但流程之外,还有其他因应用场景而存在的好处

      1. 字节码为运行时提供了一个更加安全和控制的环境,可以有效地隔离恶意代码和系统资源。V8引擎可以更容易实施安全策略和内存管理,毕竟我们不知道用户会传递进来什么样子的代码
      2. 还有就是程序员核心能力之一的调试,通过字节码可以更容易地实现代码的逐步执行和调试断点,如果在机器代码中进行的话会很复杂。经过综合的权衡,V8引擎最终在这方面做出了取舍
  • 通过这两个问题,我们对于V8引擎的处理流程就都能够了解得更加清晰

2.1.3. 全局对象

  • 在JS源代码到AST抽象语法树的Parse阶段,会给我们创建一个全局对象(GlobalObject),在这里面存放了String,Date,Number等全局方法,这也就是我们为什么能在JS文件中,直接写上类似以下的代码
    • 连着我们的顶级对象window也在里面,而window对象里就是指向于自己的this,这就是为什么我们可以window.window.window.xxxx一直无限套娃下去的原因
    • 全局对象的性质,让这些方法不必实例化也不必导入就能够直接使用。这就是这些方法的真相
String.fromCharCode(num1)
Date.now();
Number.parseInt(string, radix)
//或者是控制台打印
console.log()
  • 而v8为了执行这些代码,v8引擎内部会有一个执行上下文栈(函数调用栈),这个思想性质在Vue中也有体现
    • Vue的设计将这些生命周期方法集成到了组件实例的上下文中
    • 这个内容和内存的管理是有一定的关系的(堆栈结构),我们在下一章节会进行讲解,在这章中,我们就暂且跳过

三、V8引擎的架构设计

V8引擎本身的源码非常复杂,大概有超过100w行C++代码,在前面,我们通过了解它的架构,知道它是如何对JavaScript执行的,在这里我们进一步去探讨其中的架构细节,并给出对应的官方文档:

Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;

  • 函数不管有没有调用都会被转化为AST,因为即使函数没有被立即调用,它们可能在将来被用作回调或传递给其他函数。此外,函数内部可能包含需要提前处理的声明或表达式。所以解析器不能仅仅因为函数当前未被调用就忽略它们,必须分析所有代码以建立完整的程序结构
  • Parse的V8官方文档:v8.dev/blog/scanne…

Ignition作为解释器,会将AST转换成ByteCode(字节码)

  • 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
  • 如果函数只调用一次,Ignition会执行解释执行ByteCode;
  • Ignition的V8官方文档:v8.dev/blog/igniti…

TurboFan作为编译器,可以将字节码编译为CPU可以直接执行的机器码;

  • 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;

  • 但是,机器码实际上也有可能被还原为ByteCode(字节码),这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码(回炉重造),但需要注意的是,去优化步骤虽然优化后的机器码的使用被暂停,代码执行回退到解释器中执行未优化的字节码,但如果后续的性能分析认为有必要,这些代码还是会被重新编译成机器代码,因为最后的执行位置是CPU,而CPU只接受机器代码;

    • 请注意这里的重新执行,V8会在这个过程中重新收集类型信息和执行数据,这些数据将用于指导未来可能的重新优化。并且重新执行的位置是在Ignition(V8的解释器)中的。最终真正执行的位置是在CPU,这里需要注意一下
    function sum (num1,num2){
      return num1 + num2
    }
    
    //首先我们多次调用,被标记为热点函数,并且传递的内容都是number类型
    sum(20,20)
    sum(20,20)
    //当有一次,我们传递的从number类型变成string类型,之前优化的机器码不能正确处理,就会回退去优化,然后基于现有情况重新进行优化
    sum('xiaoyu','coderwhy')
    
  • TurboFan的V8官方文档:v8.dev/blog/turbof…

另外,V8引擎还包括了垃圾回收机制,用于自动管理内存的分配和释放。V8引擎使用了一种名为“分代式垃圾回收”(Generational Garbage Collection)的技术,它将堆区分成新生代老年代两个部分,分别使用不同的垃圾回收策略,以提高垃圾回收的效率。

  • 内存管理我们后续再单独来讨论学习

3.1. 预解析与全量解析

在JavaScript的解析过程中,存在两种主要的解析策略:预解析(Pre-parsing)和全量解析(Full parsing)。这两种解析方式都是解释器工作的一部分,目的是将源代码转换成抽象语法树(AST),但它们在处理代码时的侧重点和目的有所不同

  • 我们在讲述JS代码转化为AST树的时候,这是其中的一个细节之处,如果不注意,概念弄混,理解的难度就会上升
    • 预解析是一种较为轻量的解析过程,它的主要目的是快速扫描代码以提取基本的结构信息,如变量和函数声明。这种解析方式不会深入分析函数体内的具体逻辑或表达式,因此速度相对较快
    • 全量解析是一种更为深入的解析过程,它会详细分析整个代码文件,包括函数体内的所有语句和表达式。这种解析方式生成的AST更加完整,包含了代码中的所有细节
  • 这就可以解释一个很重要的疑惑点了,那就是函数没有执行,到底会不会转化为AST树,转为怎么样的AST树?这个的答案其实取决于对AST树怎么理解而已
    1. 函数没有执行,我没有用到,这时候会进行预解析,也就是我们上面所说的必须分析所有代码以建立完整的程序结构,此时的AST树是简化版本的,只有一个结构架子在
    2. 函数执行了,在预解析的基础上,会继续进行全量解析,真正的深入具体逻辑,将其转化为真实可用的完全体AST树(此时就可以进行下一步的将AST树转化为字节码)。并且如果函数在代码中被调用,它的全量解析将在调用发生前进行,确保所有内部逻辑被正确解析和优化
  • 所以这个问题的答案,在于你是否把简化版本的AST树认为是正常的AST树,这也是对性能优化的一种措施,可以显著减少解析时间和内存消耗

四、V8的转化代码过程

  • 这里在进行词法、语法分析的时候,我们可以通过对应的网站,去看如何拆解为抽象AST(语法)树的:

    • 想要对代码进行一定程度的转化,都是绕不开抽象语法树的,因为这个步骤会让结构更清晰,更好获取。包括说babel想要将ts转化为js也是绕不开的。babel在我们之后也是会涉及到的,感兴趣的可以先查
    • 拆解AST树网站
  • 我们先准备一段模板代码,接下来就用这段代码来进行一次完整的流程,看其中的转化过程

const name = "coderwhy and XiaoYu"
console.log(name)

function sayHi(name) {
  console.log("Hi " + name)
}

sayHi(name)

MDN文档对call的定义

图3-5 官方图例

  • 通过图3-5的流程中,可以看出是如何拆解构建AST抽象语法树的
  1. Blink: 这是流程的起始阶段,JavaScript代码以不同的编码(如ASCII、Latin1、UTF-8)被读入,并转换为数据流(chunk)
  2. Stream: 接收来自Blink阶段的数据,以UTF-16编码单元处理,准备进行下一步的扫描
  3. Scanner: 在这个阶段,流式传输的代码被扫描,转换成tokens(标记),这些标记是构建AST(抽象语法树)的基本元素
  4. PreParser: 对tokens进行预解析,以确定它们的结构,这有助于优化解析过程
  5. Parser: 解析tokens,构建AST。AST是源代码的树形结构的详细表现形式,用于指导后续的编译和优化
  6. Bytecode: AST最终被编译成字节码,能被V8引擎快速执行或进一步优化成机器代码

4.1. 词法分析的过程

  • 一些前置介绍,我们在前面都已经讲解了,在这里我们直接来看代码的转化过程
    • 其中每一行都是一个tokens(词法单元)
    • 下一步就是将词法单元重新拼成我们想要的结构,AST树
  • type属性帮助解析器(parser)识别每个token的角色和功能,从而进行进一步的语法分析和代码执行。我们总结其中一部分来进行参考,每个类型所对应的含义都是一部分小知识点,在JS基础中,我们都有学过,在这基础上,我们可以理解type的作用以及存在的必要
    1. Keyword: 关键字,如 const, function 等。是JavaScript语法的核心部分,指示解析器代码的结构和行为
    2. Identifier: 标识符,通常用于变量名、函数名等。是程序员定义的符号,用于表示变量存储位置或函数名称
    3. Operator: 操作符,如 =, + 等。操作符定义了一些操作,赋值、算术计算等,它们对数据执行特定的运算
    4. StringLiteral: 字符串字面量,表示源代码中直接出现的字符串值
    5. Punctuation: 标点符号,如逗号(,)、分号(;)、圆括号((, ))和大括号({, })。这些符号用于分隔代码中的各个部分,帮助解析器确定语句的开始和结束,以及组织代码块
Token(type='Keyword', value='const')            // 声明一个常量
Token(type='Identifier', value='name')          // 变量名
Token(type='Operator', value='=')               // 赋值操作符
Token(type='StringLiteral', value='"coderwhy and XiaoYu"')  // 字符串常量
Token(type='Punctuation', value=';')            // 语句结束符

Token(type='Identifier', value='console')       // 对象名
Token(type='Punctuation', value='.')            // 成员访问操作符
Token(type='Identifier', value='log')           // 方法名
Token(type='Punctuation', value='(')            // 函数调用开始括号
Token(type='Identifier', value='name')          // 参数名
Token(type='Punctuation', value=')')            // 函数调用结束括号
Token(type='Punctuation', value=';')            // 语句结束符

Token(type='Keyword', value='function')         // 函数关键字
Token(type='Identifier', value='sayHi')         // 函数名
Token(type='Punctuation', value='(')            // 参数列表开始括号
Token(type='Identifier', value='name')          // 参数名
Token(type='Punctuation', value=')')            // 参数列表结束括号
Token(type='Punctuation', value='{')            // 函数体开始括号

Token(type='Identifier', value='console')       // 对象名
Token(type='Punctuation', value='.')            // 成员访问操作符
Token(type='Identifier', value='log')           // 方法名
Token(type='Punctuation', value='(')            // 函数调用开始括号
Token(type='StringLiteral', value='"Hi "')      // 字符串常量
Token(type='Operator', value='+')               // 字符串连接操作符
Token(type='Identifier', value='name')          // 参数名
Token(type='Punctuation', value=')')            // 函数调用结束括号
Token(type='Punctuation', value=';')            // 语句结束符

Token(type='Punctuation', value='}')            // 函数体结束括号

Token(type='Identifier', value='sayHi')         // 函数名
Token(type='Punctuation', value='(')            // 函数调用开始括号
Token(type='Identifier', value='name')          // 参数名
Token(type='Punctuation', value=')')            // 函数调用结束括号
Token(type='Punctuation', value=';')            // 语句结束符

4.2. 语法分析的过程

接下来我们可以根据上面得到的tokens代码,进行语法分析,生成对应的AST树。

  • 在V8引擎中,语法分析的过程可以分为两个阶段:预处理(Pre-parsing)和解析(Parsing)。

    • 解析阶段是将tokens转换成抽象语法树(AST)的过程,而预处理阶段则是在解析阶段之前进行的,用于预处理一些代码,如函数和变量声明等。在这里的解析阶段也就是我们前期所说的预解析,这两个名字都指同一个意思
  • 对于我们的JS代码,V8引擎的解析和预处理过程如图3-6所示:

    1. 预处理阶段

    • 在预处理阶段,V8引擎会扫描整个代码,快速识别和处理那些可以独立于完整代码逻辑进行解析的结构(如变量和函数声明)

    • 在这个过程中,V8引擎会同时进行词法分析和语法分析,生成一些中间表示,以便后续使用

    • 对于我们的代码,预处理阶段不会生成任何AST节点,因为它只包含了一个常量声明和一个函数声明,而没有变量声明(var声明的变量),并且预处理阶段也是解析过程的一部分,并不是独立出去的步骤

    1. 解析阶段

    • 在解析阶段,V8引擎会将tokens转换成AST节点,生成一棵抽象语法树(AST)
    • 此时的抽象语法树,是简化版本的抽象语法树(前面有说怎么个简化情况),当我们真正进行调用的时候,会在调用前进行全量解析,AST会将细节补充完整,以准备转化为字节码

MDN文档对call的定义

图3-6 语法分析如何预解析

转化的AST树代码参考:

Program
 ├── VariableDeclaration
 │    └── Declarator
 │        ├── Identifier (name)       // 变量名
 │        └── Literal ("coderwhy and XiaoYu") // 字符串常量
 ├── ExpressionStatement
 │    └── CallExpression
 │        ├── MemberExpression
 │        │   ├── Identifier (console) // 对象名
 │        │   └── Identifier (log)     // 方法名
 │        └── Identifier (name)        // 参数名
 ├── FunctionDeclaration
 │    ├── Identifier (sayHi)           // 函数名
 │    ├── FunctionParams
 │    │   └── Identifier (name)        // 参数名
 │    └── BlockStatement
 │        └── ExpressionStatement
 │            └── CallExpression
 │                ├── MemberExpression
 │                │   ├── Identifier (console) // 对象名
 │                │   └── Identifier (log)     // 方法名
 │                └── BinaryExpression
 │                    ├── Literal ("Hi ")      // 字符串常量
 │                    ├── Operator (+)         // 字符串连接操作符
 │                    └── Identifier (name)    // 参数名
 └── ExpressionStatement
     └── CallExpression
         ├── Identifier (sayHi)        // 函数名
         └── Identifier (name)         // 参数名
  • 其中所对于的内容都进行了注释表示,但AST树的结构类型,我们以表格进行掌握
    • 但总的来说,想要看懂是不难的,具备很清晰的结构,而这些内容也不需要背,因为平时不会去写这种的,我们只需要理解其中的思想,这其中的转化过程,对我们来说就不是黑盒子的存在
结构类型描述示例
Program根节点,包含整个程序的语法结构-
VariableDeclaration变量声明部分,使用 const 声明一个常量const name = "coderwhy and XiaoYu"
ExpressionStatement包含表达式语句,表示调用表达式console.log(name)
FunctionDeclaration函数声明,定义函数及其结构function sayHi(name) {...}
BlockStatement代码块,用于包裹函数体内的语句{ console.log("Hi " + name); }
CallExpression函数调用表达式,用于执行函数调用sayHi(name)

4.3. 转化的字节码(了解)

根据上面得到的AST树,我们可以将其转换成对应的字节码。在V8引擎中,字节码是一种中间表示,用于表示程序的执行流程和指令序列。

V8引擎会将AST树转换成如下的字节码序列:

[Constant name="coderwhy and XiaoYu"] // 将字符串常量存入常量池
[SetLocal name]                       // 将常量赋值给局部变量name

[LoadGlobal console]                  // 加载全局对象console
[LoadProperty log]                    // 访问console对象的log属性
[GetLocal name]                       // 获取局部变量name的值
[CallProperty 1]                      // 调用console.log方法,传递1个参数

[CreateFunction sayHi]                // 创建函数sayHi
[FunctionEnter]                       // 进入函数体
    [LoadGlobal console]              // 内部:加载全局对象console
    [LoadProperty log]                // 内部:访问console对象的log属性
    [LoadConstant "Hi "]              // 加载字符串常量"Hi "
    [GetLocal name]                   // 获取函数参数name
    [Concatenate]                     // 连接字符串"Hi "和变量name
    [CallProperty 1]                  // 调用console.log方法,传递1个参数
[FunctionExit]                        // 退出函数体

[GetLocal sayHi]                      // 获取函数sayHi
[GetLocal name]                       // 获取局部变量name
[CallFunction 1]                      // 调用函数sayHi,传递1个参数
[Return]                              // 返回控制权到全局执行环境
  • 对于其中的各种单词含义,我们也还是进行一个表格的总结,当做查阅文档来进行辅助理解
    • 字节码只是过渡的阶段,我们只需要做到有所了解就可以了
字节码指令含义
[Constant ...]将一个值存储到常量池中。
[SetLocal ...]将一个值赋给一个局部变量。
[GetLocal ...]从局部变量中获取一个值。
[LoadGlobal ...]加载一个全局对象。
[LoadProperty ...]从对象中加载一个属性。
[CallProperty ...]调用一个对象的方法属性,传递指定数量的参数。
[CreateFunction ...]创建一个函数。
[FunctionEnter]标记函数体的开始。
[FunctionExit]标记函数体的结束。
[LoadConstant ...]加载一个常量值。
[Concatenate]连接两个字符串值。
[CallFunction ...]调用一个函数,传递指定数量的参数。
[Return]从当前函数或程序段返回。
  • 在解释器中,我们有说过,存在一个字节码执行过程,这和最终的CPU执行阶段是不同的。在这里进行的字节码执行,是为了方便我们的调试等功能,等下会结合机器码通过图片展示出来

4.4. 生成的机器码(了解)

在V8引擎中,机器码是通过即时编译(Just-In-Time Compilation,JIT)技术生成的。

  • JIT编译是一种动态编译技术,它将字节码转换成本地机器码,并将其缓存起来以提高代码的执行速度和性能。
  • JIT编译器可以根据运行时信息对代码进行优化,并且可以根据不同的平台和硬件生成对应的机器码。

具体的生成过程如下:

4.4.1. 优化阶段

  • 在优化阶段,V8引擎会根据代码的运行时信息生成更优化的机器码,以提高代码的执行效率和性能

  • 在这个阶段,V8引擎会通过分析代码的执行路径、类型信息、控制流程等,生成一些高效的机器码,并且可以进行多次优化,以获得更高的性能

  • 在优化阶段,V8引擎会使用TurboFan编译器来生成机器码,也就是我们文章开头中所描述的步骤

TurboFan编译器优化

图3-7 TurboFan编译器优化

  • 如图3-7,TurboFan是一个基于中间表示(Intermediate Representation,IR)的编译器,它可以将字节码转换成高效的机器码,并且可以进行多层次的优化,包括基于类型的优化、内联优化、控制流优化、垃圾回收优化等

通过机器码的生成过程,我们可以看到V8引擎是如何根据代码的运行时信息生成高效的机器码,并且可以多次优化,以获得更高的性能

  • 在后续的执行过程中,V8引擎会将机器码缓存起来,以提高代码的执行速度和性能
  • 在字节码到机器码的过程中,分别存在了对于的优化策略,我们画图将其抽离出来,如图3-8

MDN文档对call的定义

图3-8 字节码优化为机器码过程

后续预告

  • 我们本章节深入学习了JS引擎到底是怎么处理代码的,从整体的流程中入手,将每一个步骤拆分成块,然后在块的基础上,继续深入探索其优化的过程,都是怎么做以及为什么这么做
  • 那在接下来的文章中,我们就要开始学习V8引擎对于内存的管理了,JS中的内存是怎么样的,当我们在调用代码的时候,代码是会一直占据内存吗?以及JS对于内存的回收算法是怎么做的
    • 我们到时候会以函数执行来举例,因为函数是JS中最核心的内容,在以后的项目中,更是会构成函数式的主流写法
    • 函数会涉及到很多内容,函数内外的作用域一样吗?什么是作用域?
    • 在执行上下文栈的时候,如何区分堆栈内存所存放的内容类型?
  • 以上的这些问题,将会在下一章节中一起探索和揭晓
  • 后续JavaScript高级知识技术会持续更新,如果喜欢我们的文章,欢迎关注、点赞、转发、评论,你们的支持是我们最大的动力