重学JavaScript(三) | js的V8引擎

345 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

前言

之前在重学JavaScript | 浏览器的渲染及解析 - 掘金 (juejin.cn)说到过当浏览器遇到js代码的时候,它是交由javaScript引擎来解析和运行的,身为一名前端,自然要了解下这个神秘的js引擎

JavaScript引擎

为什么我们执行代码要用到javaScript引擎呢

  • 重学JavaScript | 重新认识计算机语言 - 掘金 (juejin.cn) 之前的这篇文章里说到,高级编程语言的计算机是不认识的,都需要转换成对应的机器指令才可以
  • 我们编写的javaScript代码,不管是在浏览器或者node中执行都是最终要被cpu执行的
  • cpu只能识别机器语言,也就是自己的指令集,转换为这些才能被cpu执行

综上所属,我们需要一个JavaScript引擎来将我们写的js代码转换为cpu指令来执行,此外,还负责执行代码、分配内存以及垃圾回收

常见的javaScript引擎

简单了解一下常见的js引擎

  • SpiderMonkey: 是第一个js引擎,由js之父Brend Eich开发
  • JavaScriptCore: 是apple公司开发的一款引擎,主要用于wkit渲染引擎的浏览器
  • Charkra: 微软开发的用于ie浏览器的js引擎
  • V8: 号称目前最强大JavaScript引擎,由Google开发,广泛应用于目前绝大多数浏览器

js引擎和浏览器内核的关系

webkit为例,webkit事实上是由两部分组成的

  • webCore:负责HTML代码的解析,布局,渲染等工作
  • JavaScriptCore: 负责解析和执行js代码

如下图

image.png

再来看一下微信小程序的例子

image.png

这个渲染层和逻辑层是不是就是webkit的两块,实际上,小程序的js代码就是由JavaScriptCore来负责执行的

相信现在你已经了浏览器内核和javaScript引擎的关系了,接下来让我们仔细介绍下我们的今天的主角,强大的V8引擎

强大的V8引擎

先来看下v8官方是怎么描述它的

V8是Google的开源高性能JavaScript和WebAssembly引擎,用C++编写。它用于Chrome和Node.js等。

它实现了 ECMAScript 和 WebAssembly,并在 Windows 7 或更高版本、macOS 10.12+ 以及使用 x64、IA-32、ARM 或 MIPS 处理器的 Linux 系统上运行

V8 可以独立运行,也可以嵌入到任何C++应用程序中。

V8引擎的基本原理

来看一张图,这是V8引擎解析js代码的全过程

image.png

大家一定很疑惑吧,图上的parseignition以及Turbofan分别代表什么呢?

V8引擎本身的源码结构非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:

  • Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;如果函数没有被调用,那么是不会被转换成AST的
  • Ignition是一个解释器,会将AST转换成ByteCode(字节码),同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
  • TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码; 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能
  • 但是,当优化的代码的变量类型发生变化时,字节码还是会转换成机械码的,比如如下的代码
function sum(a,b) {
    return a+b
}
sum(1,2) // Number类型的数据
sum(2,3) // Number类型的数据
sum(2,2) // Number类型的数据

// 我们这个sum函数的参数一直是两个Number类型的数据相加,这个函数就会被标记为热点函数,就会经过`TurboFan`转换成优化的机器码

 sum(2,'777') // Number类型的数据和字符串相加
// 但是当string类型和 Number类型相加时,之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码

我们再来分析下V8引擎的基本原理里的流程图

  1. parse对js代码进行词法解析和语法解析,最终转换为AST抽象语法树
// 这里是代码块2:


 let name = 'juejin'
 // 1.进行词法解析,提取let,name,=,'juejin'
 
 // 上述代码的tokens
tokens = [ { type:"keyword", value: let, } { type:"identifier", value: "name", } ... ]
// 2.再根据tokens来进行语法分析,比如type,value分别是什么,然后转换为AST语法树  

// 3. let name = 'juejin' 对应的JSON
{
 "type": "Program",
 "start": 0,
 "end": 19,
 "body": [
   {
     "type": "VariableDeclaration",
     "start": 0,
     "end": 19,
     "declarations": [
       {
         "type": "VariableDeclarator",
         "start": 4,
         "end": 19,
         "id": {
           "type": "Identifier",
           "start": 4,
           "end": 8,
           "name": "name"
         },
         "init": {
           "type": "Literal",
           "start": 11,
           "end": 19,
           "value": "juejin",
           "raw": "'juejin'"
         }
       }
     ],
     "kind": "let"
   }
 ],
 "sourceType": "module"
}
// 4. let name = 'juejin' 对应的AST树  看下图


image.png

可通过AST explorer来进行js代码和ast树的转换

  1. IgnitionAST转换成ByteCode(字节码),代码的运行环境是不一定的,而不同的环境他有不同的CPU架构,他们执行的机械指令是不一样的,而Ignition他不知道该转换成什么机械指令的代码,而ByteCode(字节码)他相当于是跨平台的,v8引擎就会把字节码转换成对应的汇编代码然后再转换成相应平台(window、Linux)的CPU指令(0101)
  2. 中途会遇到多次执行的代码,通过TurboFan进行收集和优化

V8引擎的执行细节

先来看一张官方的执行图

image.png

可以看出来官方的图上多了parser解析前的过程,我们接下来来解释下这部分的过程

我们浏览器遇到JS代码后,V8引擎是怎么做的

  1. Blink(内核)对HTML代码进行解析,遇到js代码后将源码交给V8引擎
  2. Stream(数据流)获取源码并进行编码转换
  3. Scanner(扫描器) Scanner会进行词法分析(lexicalanalysis),词法分析会将代码转换成tokens;
  4. tokens会被转换成AST树,经过ParserPreParser,Parser就是直接将tokens转成AST树架构;

没错,3,4就是代码块2里描述的过程

ParserPreParser(解析和预解析)

JavaScript代码在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率,所以V8引擎就实现了LazyParsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析 也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行;

举个栗子

// 代码块3
function f() {
    function c() {
    }
}

// 例如函数f中定义了一个函数c,这个时候就不解析函数c,只会对它进行预解析