JS之JavaScript代码是如何被浏览器解析执行的

490 阅读5分钟

这是我参与更文挑战的第28天,活动详情查看: 更文挑战

编程语言分为编译型语言和解释型语言。

  • 编译型语言是在代码运行前编译器直接将代码转换成机器码。优势之处是编译一次随后就可以拿着最终结果重复程序运行,而无需再次翻译,执行程序效率要高于解释型。
  • 解释型语言必须要有一个解释器,解释器会读取程序代码,一边翻译一边执行程序,优势之处是代码运行是依赖于解释器,不同平台有对应版本的解释器,故而代码是能够直接跨平台程序运行的,不足之处是每次执行程序都必须要翻译,执行程序效率低于编译型。

JavaScript是解释型语言,而V8 是众多浏览器的 JS 引擎中性能表现最好的一个,并且它是 Chrome 的内核,Node.js 也是基于 V8 引擎研发的。

V8 引擎执行 JS 代码都要经过哪些阶段?

  1. Parse阶段:V8引擎负责将JS代码转换成AST(抽象语法树)
  2. Ignition阶段:解释器将AST转换为字节码,解析执行字节码也会为下一个阶段优化编译提供需要的信息。
  3. TurboFan阶段:编译器利用上个阶段收集的信息,将字节码优化为可以执行的机器码
  4. Orinoco 阶段:垃圾回收阶段,将程序中不再使用的内存空间进行回收。

主要的就是前三个阶段。

生成 AST

Eslint 和 Babel 这两个工具和生成AST脱不了关系。V8引擎就是通过编译器(Parse)将源代码解析成AST的。

应用场景

  • JS反编译,语法解析
  • Babel编译ES6语法
  • 代码高亮
  • 关键字匹配
  • 代码压缩

生成AST有两个阶段,一是词法分析,二是语法分析

  1. 词法分析:这个阶段将代码拆分成最小的、不可再拆分的词法单元(token),比如var a = 1;通常就会被分解成vara=1;这五个词法单元,空格在JS中是被忽略的。
  2. 语法分析:这个过程是将词法单元转换成一个元素逐级嵌套组成的代表程序语法结构的树,这个树叫抽象语法树。

看看解析成抽象语法树之后是什么样子。

var a = 1;

转换成 AST 抽象语法树之后返回的 JSON 格式

{
  "type": "Program",
  "start": 0,
  "end": 10,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 0,
      "end": 10,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 4,
          "end": 9,
          "id": {
            "type": "Identifier",
            "start": 4,
            "end": 5,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 8,
            "end": 9,
            "value": 1,
            "raw": "1"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}
function sum (a,b) {
  return a + b;
}

转换成 AST 抽象语法树之后返回的 JSON 格式

{
  "type": "Program",
  "start": 0,
  "end": 38,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 38,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 12,
        "name": "sum"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [
        {
          "type": "Identifier",
          "start": 14,
          "end": 15,
          "name": "a"
        },
        {
          "type": "Identifier",
          "start": 16,
          "end": 17,
          "name": "b"
        }
      ],
      "body": {
        "type": "BlockStatement",
        "start": 19,
        "end": 38,
        "body": [
          {
            "type": "ReturnStatement",
            "start": 23,
            "end": 36,
            "argument": {
              "type": "BinaryExpression",
              "start": 30,
              "end": 35,
              "left": {
                "type": "Identifier",
                "start": 30,
                "end": 31,
                "name": "a"
              },
              "operator": "+",
              "right": {
                "type": "Identifier",
                "start": 34,
                "end": 35,
                "name": "b"
              }
            }
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

前端常用的Babel工具,作用是如果现在浏览器不支持ES6语法,将ES6语法的代码其转换为ES5语法的代码以供浏览器执行。过程就是先将ES6代码解析成AST(抽象语法树),再将ES6语法的抽象语法树转成ES5语法的抽象语法树。ESlint的原理也大致相同,也是将源码先转换成抽象语法树,再利用抽象语法树来监测代码规范。

为了更方便理解,这里有一个AST在线转换网址,可以自己试试

生成字节码

将抽象语法树转换成字节码,也就是Ignition阶段,也就是将AST转换成字节码。

之前的V8版本是直接将AST转换成机器码,没有生成字节码这个阶段,但是如果没有字节码这个阶段会有一些问题。直接转换会让内存占用过大,因为抽象语法树全部转换成了机器码,而机器码要比字节码占用的内存多很多。有些场景下,代码没必要生成机器码,减少内存占用过大的问题。

所以后来V8又加入了这一个过程,**V8 重新引进了 Ignition 解释器,将抽象语法树转换成字节码之后,内存占用显著下降。**同时也可以使用 JIT 编译器做进一步的优化。

其实字节码是介于 AST 和机器码之间的一种代码,需要将其转换成机器码后才能执行,字节码可以理解为是机器码的一种抽象。Ignition 解释器除了可以快速生成没有优化的字节码外,还可以执行部分字节码。

生成机器码

在Ignition解释器处理完之后,如果发现一段代码被重复执行多次的情况,生成的字节码和分析数据会传给Turbo编译器,他会根据分析数据生成优化好的机器码,这样再执行这段代码只需要直接执行编译后的机器码,这样的性能更好。

这里简单说一下 TurboFan 编译器,它是 JIT 优化的编译器,因为 V8 引擎是多线程的,TurboFan 的编译线程和Ignition的生成字节码不会在同一个线程上,这样可以和 Ignition 解释器相互配合着使用,不受另一方的影响。

总结

image-20210628233311031

通过V8引擎,JavaScript源代码进来之后,先将JS转换成AST抽象语法树,再通过Ignition解释器将AST转换成字节码,再通过TurboFan将字节码转换成机器码(如果有反复使用的代码就生成优化好的机器码,下次执行运行生成的机器码。),然后再进行垃圾回收。