一段JavaScript代码是如何运行的?

0 阅读3分钟

众所周知JavaScript 是解释型语言,解释型语言都有编译器,通过谷歌浏览器的编译器是V8引擎 生成字节码并被计算机认识并执行。

V8引擎主要运行代码行为是: 源代码(.js 文件)-->词法分析(解析器(parser)-->AST(语法分析)-->字节码(Ignition 生成)-》执行()

词法分析

// 我是注释
var answer = 6 * 7;

像上面一段代码

会被V8引擎去除无用的的注释等通过解析器(parser)分解为最小的词法单元token(parser), 使用正则表达式,匹配出最小单元的Token,让V8更快的扫描并进行解析

image.png 这段代码被拆解为:token 数组,并标识了type类型和value

语法分析AST

众所周知 是静态语音,类似VsCode 编辑器虽然也会对代码编写时的语法的静态检查,但是是非常轻量级,并且不涉及到编译的过程,所以在词法分析的下一步进行编译阶段的语法分析AST以保证代码语法结构的正确性,同时为后续的语义分析等提供结构性的数据。下面是一段代码AST 结构图:

image.png 上图是根据上一步的词法分析Token数组生产的Json树状结构树,其主要包含的信息有:"VariableDeclaration"(变量声明)"BinaryExpression"(计算的二元表达式),"name"(标识符的名称),"value"(字面量的值),"operato"(表达式的操作符)等。在这一阶段如果发生错误代码加载会在这步停止并抛出异常如: 输出: Unexpected token at line 1, column 5

生成字节码

JS 字节码 的生成是由V8(Ignition 解释器完成),熟悉Java 的都知道,Java的字节码 经过AOT存在.class文件中,但JavaScript 为了快速执行把字节码存在了内存中,然后经过JIT 生成机器码。到这里我们发现为什么大部分语言都是先是生成字节码再生成机器码呢。其实早期V8 引擎也是直接生成机器码,用空间换时间,执行效率高,但同时也带来另一个问题,当内存不足时,就会造成失败和卡顿。有了字节码以后做了中间流程后,引擎可以检测热点(hot code)代码,由TurboFan 编译器再生成机器码,提升运行时效率,非热点代码的字节码可以立即执行,并且生成字节码的速度远大于生产机器码的速度,这样的一顿操作后V8就可以平衡代码运行时的时间和空间的复杂度。 下面是模拟字节码和机器码运行的测试代码:

// 计算密集型函数(模拟热点)
function computeIntensive() {
  let result = 0;
  for (let i = 0; i < 10000000; i++) {  // 增加循环次数以放大差距
    result += Math.sin(i) * Math.cos(i);
  }
  return result;
}

// 冷启动测试:第一次执行(主要使用字节码)
console.time('Cold Start (Bytecode)');
computeIntensive();
console.timeEnd('Cold Start (Bytecode)');

// 热启动测试:多次执行后(触发 JIT,机器码优化)
console.time('Hot Start (Machine Code)');
for (let i = 0; i < 10; i++) {
  computeIntensive();  // 重复执行,V8 会优化成机器码
}
console.timeEnd('Hot Start (Machine Code)');

在终端运行:node --trace-opt test.js(Node.js:是一个基于 V8 JavaScript 引擎的运行时环境),执行结果如下图:

image.png 冷启动的时候执行了,耗时:1.486s ,但代码立即执行了一次。 Hot Code热启动 耗时:14.955s,但执行了100,000,000次,可以看出V8 通过分层执行字节码和机器码以保证代码执行的效率;

执行

生成后的机器码和字节码全部存在内存里,当一个执行命令执行到一个函数后,V8 runtime 会检测是调取机器码还是字节码,并把地址信息给到Cpu,来执行该地址的指令