一文了解V8 执行 JavaScript 代码的完整流程

586 阅读5分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力。 接触一个新技能(知识点),我们往往会经历下面这样的思考流程:它是什么,我为什么要了解或学习它,学习它能给我带来什么收益?发问三连击。好,那么下文也按照这三点来一一说明,为什么需要了解学习V8。

什么是V8

V8是谷歌发布的一个开源JavaScript引擎,底层使用C++编写,用来执行JavaScript(简称'JS'),是Chrome浏览器Chrome内核的JS引擎;我们熟知的nodejs也使用V8来执行js。 image.png 我们可以先简单的把V8当成是一个翻译程序,将工程师编写的程语言 JavaScript,翻译成机器能够理解的机器语言。

为什么需要了解V8的执行流程

目前业内有不少JS引擎,如SpiderMonkeyjsCorerhino等,但其中以V8性能最为出色,应用最为广泛,代表这业内最高的水平;如在国内,pc端的360、QQ、搜狗等常用浏览器,以及安卓系统的原生浏览器,都使用了Chrome内核;从列举的浏览器可以看出,作为一名前端开发者,我们写的js代码,大多数都跑在v8上,所以我们有必要了解下V8是如何执行js的。

执行流程

从学习JavaScript的第一天起,我们就被告知,js是一门解释型、动态脚本语言。不同于Java、C++等编译型,运行前需要编译器编译成机器能懂的二进制文件;而解释型语言,每次运行都需要解释器对其进行动态解释和执行。

image.png 从上图可以看到,V8在执行过程需要解释器、编译器的参与,那么它们是如何配合去执行一段 JavaScript代码的呢? 接下来我们就按照上图来一一分解其执行流程。

生成抽象语法树(AST)

第一步将js源码生成AST。对于高级编程语言,解释器和编译器是不能理解的,首先得把它转为让解释器、编译器都能懂的东西——AST。 借助在线工具,我们来看看js代码对应的AST是什么:

function sayHello() {
  var msg = '你好,掘金!'
  return msg;
}
sayHello();

对应的AST是这样的:

{
  "type": "Program",
  "start": 0,
  "end": 70,
  "body": [
    {
      "type": "FunctionDeclaration",
      "start": 0,
      "end": 58,
      "id": {
        "type": "Identifier",
        "start": 9,
        "end": 17,
        "name": "sayHello"
      },
      "expression": false,
      "generator": false,
      "async": false,
      "params": [],
      "body": {
        "type": "BlockStatement",
        "start": 20,
        "end": 58,
        "body": [
          {
            "type": "VariableDeclaration",
            "start": 24,
            "end": 42,
            "declarations": [
              {
                "type": "VariableDeclarator",
                "start": 28,
                "end": 42,
                "id": {
                  "type": "Identifier",
                  "start": 28,
                  "end": 31,
                  "name": "msg"
                },
                "init": {
                  "type": "Literal",
                  "start": 34,
                  "end": 42,
                  "value": "你好,掘金!",
                  "raw": "'你好,掘金!'"
                }
              }
            ],
            "kind": "var"
          },
          {
            "type": "ReturnStatement",
            "start": 45,
            "end": 56,
            "argument": {
              "type": "Identifier",
              "start": 52,
              "end": 55,
              "name": "msg"
            }
          }
        ]
      }
    },
    {
      "type": "ExpressionStatement",
      "start": 59,
      "end": 70,
      "expression": {
        "type": "CallExpression",
        "start": 59,
        "end": 69,
        "callee": {
          "type": "Identifier",
          "start": 59,
          "end": 67,
          "name": "sayHello"
        },
        "arguments": [],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}

上面生成的AST是个JSON格式的数据,有点复杂,我们可以看下图经过收起简化的,body里有两个对象,刚好和实例代码的两个部分(函数声明、调用)对应上了,其实你也可以把 AST 看成代码的结构化的表示,编译器或者解释器后续的工作都需要依赖于 AST,而不是源代码。 image.png 其实AST是很重要的一种数据结构,我们前端开发在日常开发中,几乎每天都在被动或主动的和它打交道。比如我们在开发环境编写ES6+代码时,生产环境需要将其转为ES5代码时使用的Babel,就是依赖AST的代码转换器:首先把ES6+源码转换为AST,然后再将ES6+的AST转换为ES5语法的AST,最后利用ES5的AST生成JavaScript源代码。我们使用来检查语法的ESLint,也是用了AST。 生成 AST 需要经过两个阶段,分别是词法分析和语法分析。 首先进行词法分析:将一行行的源码拆解成一个个token。token是指语法上不可能再分的、最小的单个字符或字符串。如下图,关键字、标识符、赋值以及字符串部分都是一个token.

image.png

拆解token后,接着进行语法分析,把上一步生成的token数据,根据语法规则转为AST。如果源码符合语法规则,这一步就会顺利完成。否则这一步就会终止,并抛出一个“语法错误”。

生成了AST后,那接下来 V8 就会生成该段代码的执行上下文。

生成字节码

经过一系列操作后,得到了AST和执行上下文后,解释器就会根据AST生成字节码,并解释执行字节码。这里可能会有疑问,不是应该转为执行效率更高的机器码吗。事情是这样子的,一开始V8是把AST直接转为机器码的,让它获得了显著的性能提升;但随着移动设备的普及,特别是在手机端,内存空间有限,我们知道机器码其实就是一堆二进制数据,很占空间,所以导致V8的这种方式,极大的消耗内存。后来为了解决这个问题,V8引入了字节码。因为字节码体积比机器码的小得多,减少了系统内存的占用。

执行代码

有了字节码后,接下来解释器就开始解释执行字节码了。在解释器执行字节码的过程中,如果发现有热点代码,就是某段代码被重复执行了多次,就称为热点代码,那么编译器就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。 至此,V8执行JavaScript代码的流程已讲完,希望能给到大家一些思考和帮忙!