[原理剖析]JS代码在V8中的执行过程

606 阅读4分钟

JavaScript执行过程

v8.png

生成抽象语法树

分词/词法分析、

这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)。举个栗子 var name = '惊鸿'。

[    {        "type": "Keyword",        "value": "var"    },    {        "type": "Identifier",        "value": "name"    },    {            "type": "Punctuator",        "value": "="    },    {        "type": "String",        "value": "'惊鸿'"    }]

解析/词法分析

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree, AST),
在webpack的核心原理中就用到babel将代码生成ES6的抽象语法树,再转为ES5的抽象语法树,最后转为ES5的代码。

//AST抽象语法树
{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "name"
          },
          "init": {
            "type": "Literal",
            "value": "惊鸿",
            "raw": "'惊鸿'"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "script"
}

可视化展现AST:
ast.png
生成分词,解析代码网站
生成可视化AST

生成字节码

字节码

字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
我们来打印一下字节码(需要安装V8的调试工具)。

安装V8调试工具

1.安装jsvu

npm i jsvu -g

2.运行jsvu选择操作系统版本

jsvu

image.png

3.安装v8和v8-debug引擎

image.png

4.配置环境变量

安装的引擎位置会在C:\Users\你的系统用户名\ .jsvu中, image.png 在环境变量的path中加入引擎的安装地址。

5.运行打印

语法:v8-debug 指令 文件
v8-debug --help可以查看更多调试指令
v8-debug --print-bytecode .\test.js //打印字节码
Bytecode length: 18
Parameter count 1
Register count 3
Frame size 24
Bytecode age: 0
         0000025800253746 @    0 : 13 00             LdaConstant [0]
         0000025800253748 @    2 : c3                Star1
         0000025800253749 @    3 : 19 fe f8          Mov <closure>, r2
         000002580025374C @    6 : 65 59 01 f9 02    CallRuntime [DeclareGlobals], r1-r2
         0000025800253751 @   11 : 13 01             LdaConstant [1]
         0000025800253753 @   13 : 23 02 00          StaGlobal [2], [0]
         0000025800253756 @   16 : 0e                LdaUndefined
         0000025800253757 @   17 : a9                Return
Constant pool (size = 3)
0000025800253711: [FixedArray] in OldSpace
 - map: 0x025800002239 <Map(FIXED_ARRAY_TYPE)>
 - length: 3
           0: 0x025800253705 <FixedArray[1]>
           1: 0x0258002536a1 minkernel\crts\ucrt\src\appcrt\convert\isctype.cpp(36) : Assertion failed: c >= 
-1 && c <= 255
minkernel\crts\ucrt\src\appcrt\convert\isctype.cpp(42) : Assertion failed: c >= -1 && c <= 255
minkernel\crts\ucrt\src\appcrt\convert\isctype.cpp(36) : Assertion failed: c >= -1 && c <= 255
minkernel\crts\ucrt\src\appcrt\convert\isctype.cpp(42) : Assertion failed: c >= -1 && c <= 255
<String[2]: u#\x60ca\x9e3f>
           2: 0x02580000513d <String[4]: #name>
Handler Table (size = 0)
Source Position Table (size = 0)

为什么需要字节码?

在最初的V8引擎是直接将AST转为机器码,没有转字节码这一步,那么为什么要多出这一步,直接转机器码不香吗?
机器码的执行速度更快,但是机器码所占用的内存空间远远大于字节码,引入字节码,可以在内存和执行速度之间做一个调节

字节码的存储位置?

字节码和机器码以哈希表的形式存在v8的堆内存中。

执行字节码

解释器Ignition

生成字节码和解释执行字节码。

热点代码

一段代码被重复执行多次,会被解释器标记为热点代码。

编译器TurboFan

将热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码。

即时编译技术JIT

编译器和解释器配合的一种技术,在这指的就是,解释器Ignition在解释执行一段字节码时,发现某一部分代码'发热了',编译器TurboFan把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。

优化与反优化

将热代码转为机器码存储的过程为优化,
反优化则为丢弃生成的机器码,回退到AST生成字节码的过程。对反优化感兴趣的同学可以看看谷歌编译团队写的反优化文章 此处还是以V8的调试为例子。

\\test.js
let j = 1;
for(let i = 0;i<100000;i++){
 j = j+1; //这里j = j + 1跑了十万遍,“跑热了”
}
j = '靓仔' 
j = j+1;   //j的类型发生了改变,进行反优化

v8-debug --trace-opt .\test.js //trace optimized compilation  跟踪优化
[marking 0x006900253789 <JSFunction (sfi = 00000069002536CD)> for optimization to TURBOFAN, ConcurrencyMode::kConcurrent, reason: small function]
[compiling method 0x006900253789 <JSFunction (sfi = 00000069002536CD)> (target TURBOFAN) using Turbofan OSR] 
[optimizing 0x006900253789 <JSFunction (sfi = 00000069002536CD)> (target TURBOFAN) - took 0.181, 6.474, 0.462 ms]
[completed optimizing 0x006900253789 <JSFunction (sfi = 00000069002536CD)> (target TURBOFAN)]
v8-debug --trace-deopt .\test.js //trace deoptimization      跟踪反优化
[bailout (kind: deopt-eager, reason: Insufficient type feedback for binary operation): begin. deoptimizing 0x01f4002537d9 <JSFunction (sfi = 000001F4002536CD)>, opt id 0, node id 52, bytecode offset 39, deopt exit 7, FP to SP delta 80, caller SP 0x001a58dfe428, pc 0x7ffe400843fe]

这里的结论是尽量不要随意改变变量类型,会对性能造成一定的损耗。
如有错误,欢迎各位同学帮忙指正。