"var a = 1"到底经历了什么

551 阅读4分钟

本篇文章不会涉及到堆栈,异步队列等知识,我们只探讨从代码到执行,JS引擎做了哪些工作。

众所周知,JavaScript是一门解释型语言,只有当JS引擎将要执行代码时,才会去对代码进行解析编译。

那么在JS引擎将代码读取到执行这中间到底经过了怎样的一些操作?

我们以最熟悉的JS引擎V8简单描述一下。

V8引擎

20190716ignitionturbofanpipeline.png

如上图所示,引擎主要分为三步进行编译。

1. 解析器(parser)

解析器将源码转换为抽象语法树(AST)

2. 解释器(interpreter)

解释器将抽象语法树转换为字节码(bytecode),并执行。

3. 优化编译器(optimizing compiler)

为了提高运行速度,在解释器执行字节码时,会收集分析数据,分析数据传到优化编译器之后,编译器会生成优化后的机器码。 当优化失败时,会回退到原来的字节码。

你可能对有些名词还不是很熟悉,别急,我们之后会详细的介绍。

如果我是引擎

接下来,我们会模拟这个过程,并以引擎的视角去解释为什么。

以下面这段代码为例

var a = 1;

1. 将代码转换为抽象语法树

我是JS引擎,我拿到了一串字符串,我想搞懂它想让我干什么。

首先,我要读懂这串字符串。OK,让我们来想象一下,当你拿到一封信或者一句话时,你想知道它想表达什么,那你会怎么做? 你可能会说:废话,当然是通读一遍! 没错,就像我们理解文字一样,JS引擎会去分析这串代码,会对其进行词法分析(Lexical analysis),也就是分词操作。 那上面一段代码就会被拆分为vara=1这些词(token)。

当分词结束后,也就是我们将这句话读完了,去掉了多余的留白,并且知道没有不认识的字。接下来就是理解这段话,也就是我们的断句。对应的引擎过程就是语法分析(Syntax analysis)。

语法分析会将分词转换为树形结构,并且验证其合法性。

而生成的树形结构被称为抽象语法树,以上代码会被转换为类似的结构。

{
  "type": "Program",
  "start": 0,
  "end": 189,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 179,
      "end": 188,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 183,
          "end": 188,
          "id": {
            "type": "Identifier",
            "start": 183,
            "end": 184,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 187,
            "end": 188,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}

-- AST Explorer

2. 将抽象语法树编译为机器语言

光是抽象语法树并不能让引擎去执行这些逻辑,首先抽象语法树只是一种数据结构,其次我们需要生成一些机器能识别执行的代码。所以接下来的工作就是将抽象语法树转换为机器能执行的代码。

首先,先让我们忘记上面那张流程图。

当引擎获取到AST后,会调用编译器将AST转换为机器码,在V8旧版本(v5.9之前)使用的基本编译器是Full-codegen。并且在执行机器码时会收集分析数据并且异步地重新编译生成优化后的机器码,旧版本使用的优化编译器是Crankshaft

整个流程大概像这样:

捕获.PNG

到这里已经能够实现将源码解析并且执行,但是Full-codegen+Crankshaft也有不少缺点。

由于直接将AST生成机器码,所以每次有新的语言特性,需要对不同的9个平台架构进行特殊编码。这使得扩展变得极其困难。这是其一,其次是编译器直接生成机器码,大量的机器码占用了大量的内存。

针对这些问题,V8团队进行了一些优化,在源码到编译器之间加了一层解释器(Interpreter),叫做Ignition

Ignition会生成体积较小的BytecodeBytecode其实就是抽象的机器码,为什么要抽象一层出来,就是为了解决增加新特性时要针对不同平台结构特殊编码的问题。而与之配套的优化编译器叫做TurboFan

Bytecode is an abstraction of machine code

所以我们生成的AST先被转换为抽象的Bytecode,然后在根据运行平台被编译成可执行的机器码。

1_aal_1sevnb4UaX8AvUQCg.png

抽象化后,优化变得更加容易,只需要对Bytecode进行优化,就能反映到生成的机器码中。所以在生成Bytecode时,实际上进行了许多优化。最后生成的也是优化后的Bytecode

所以在V8 v5.9之后,Ignition + TurboFan 取代了 Full-codegen + Crankshaft


参考文章