1 V8核心模块

jsv8

1.1 解析器(Parser)

  • 在 V8 引擎拿到 JS 代码之后,解析器(Parser)会对其进行词法分析语法分析
    • 词法分析:将JS代码拆分为{ type: '类型/属性', value: '对应字符值' }
    • 语法分析:
      • 在JS代码被转为Token后,解析器继续生成对应的AST(抽象语法树)
      • 通过词法作用域规则,确定变量的作用域关系,即作用域链
      • AST应用于Vue、React中虚拟DOM的表示,以及Babel对ES6转译过程的描述
  • V8通过预解析提升解析效率

1.2 解释器(Ignition)与字节码

  • 在解析器将JS代码解析为AST后,解释器(Ignition)根据AST来生成字节码

    1. 解释器在将 AST 转为字节码 之后,逐行执行,这个执行过程肯定是比直接执行机器码要慢的,所以在执行方面,速度上会比较慢;
    2. 但是 JS 源码通过解析器转 AST,然后再通过解释器转字节码,这个转译过程是比编译器直接将 JS 源码转机器码要快很多的,全流程看来,整个时间上是差不了多少的,但是却减小了大量的内存占用,何乐而不为。
  • 字节码:字节码其实是机器码的抽象,各种字节码的相互构成,可以实现 JS 所需的所有功能,当然首先一点,字节码比机器码占用的内存要小很多很多,基本是机器码所在内存的几十甚至几百分之一,

  • 总结:采用字节码的编译执行方案,牺牲了执行速度,提升解析编译速度,保证速度差不多的同时,减少大量内存的占用

1.3 编译器(TurboFan)与热代码

  • 接下来解决执行速度慢的问题。在 V8 模块中会有专门的监控模块,来监控同一代码是否多次被调用,如果被多次调用,那么就会被标记为热代码(HotSpot)
    1. 当存在热代码的时候,V8 会借着 TurboFan 将为热代码的字节码转为机器码并缓存下来,这样一来,当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。当然热代码相对来说还是少部分的,所以缓存也并不会占用太大内存,并且提升了执行效率,同样此处也是牺牲空间换时间。
    2. 如果热代码在某次执行的时候,突然其中的某个属性被修改了,那么编译成机器码的热代码会被退回到 AST这一步,这个时候解释器会重新解释执行被修改的代码。

1.4 总结

  • V8 对 JS 执行的过程,不仅使用到了解释器,还用到了优化编译器。这种两者结合去处理的方式(就是指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用),业界称为 JIT (Just-In-Time)。使用这种结合的方式来处理 JS,主要是利用了 AST 形成的字节码文件较(机器码)小,而通过优化编译器编译后的热代码执行效率高,两者结合,各自发挥各自的优势,将效率尽量提升到最大。

2 JS执行机制

2.1 作用域链与执行上下文

  • 二者联系:

    • 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变

    • 一个作用域下可能包含若干个上下文环境。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。

  • 作用域链和执行上下文的形成分别在哪个阶段(解析,预编译,解释执行?)?

2.1.1 作用域链

2.1.1.1 作用域

  • 定义:作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
  • 分类:
    • 全局作用域:
      • 在代码中任何地方都能访问到的对象拥有全局作用域
    • 函数作用域:
      • 是指声明在函数内部的变量,和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部。
      • 块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域,由此引出了for循环的经典面试题
    • 块级作用域:
      • 块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。
      • 声明变量不会提升到代码块顶部
      • for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

2.1.1.2 作用域链

  • 作用域链就是保存在函数内部一个私有属性[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。

  • 简单的说就是,作用域中出现的变量如果在作用域中找不到,那么就会一层一层向父作用域找,直到找到全局作用域。这样一层一层的关系,就是作用域链

  • 注意:因为作用域是在对象、函数或变量定义时确定的,所以作用域中的变量值是要到定义这个函数的作用域中找,与在何处调用是无关的

2.1.2 执行上下文

  • 定义:当函数执行的时候,会创建一个称为执行期上下文的对象(AO对象),一个执行期上下文定义了一个函数执行时的环境。 函数每次执行时,对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁
  • 对于任意一个EC,其内部都会初始化三个属性
    1. 作用域链(scope chain)
    2. VO (Variable Object)
    3. this

2.2 分析阶段

  • 分析阶段

    1. 创建分析对象: 用于处理执行时,访问变量和方法时候,根据一定规则进行作用域的访问。
    2. 预编译:提高运行时的效率,会把把代码进行预编译,如变量提升。
  • 发生在何时

    • 解析阶段
  • 分类

    • 代码执行之前
      • 声明提升
      • 函数声明整体提升
    • 函数执行之前
      • 创建一个AO
      • 找形参和变量声明,作为AO的key,值为undefined
      • 将实参与形参统一
      • 在函数体内找函数声明,将函数名作为AO对象的key,值为函数体
    • 全局
      • 创建GO
      • 找全局变量声明,作为GO的key,值为undefined
      • 找全局函数声明,作为GO的key,值为函数体

2.3 执行过程

  1. 创建执行上下文:根据分析的对象创建执行上下文
  2. 上下文入栈执行
  3. 变量赋值
  4. 方法调用