JS执行过程
Contents
1 V8核心模块
1.1 解析器(Parser)
- 在 V8 引擎拿到 JS 代码之后,解析器(Parser)会对其进行词法分析和语法分析。
- 词法分析:将JS代码拆分为
{ type: '类型/属性', value: '对应字符值' }
- 语法分析:
- 在JS代码被转为Token后,解析器继续生成对应的AST(抽象语法树)
- 通过词法作用域规则,确定变量的作用域关系,即作用域链。
- AST应用于Vue、React中虚拟DOM的表示,以及Babel对ES6转译过程的描述
- 词法分析:将JS代码拆分为
- V8通过预解析提升解析效率
1.2 解释器(Ignition)与字节码
-
在解析器将JS代码解析为AST后,解释器(Ignition)根据AST来生成字节码
- 解释器在将 AST 转为字节码 之后,逐行执行,这个执行过程肯定是比直接执行机器码要慢的,所以在执行方面,速度上会比较慢;
- 但是 JS 源码通过解析器转 AST,然后再通过解释器转字节码,这个转译过程是比编译器直接将 JS 源码转机器码要快很多的,全流程看来,整个时间上是差不了多少的,但是却减小了大量的内存占用,何乐而不为。
-
字节码:字节码其实是机器码的抽象,各种字节码的相互构成,可以实现 JS 所需的所有功能,当然首先一点,字节码比机器码占用的内存要小很多很多,基本是机器码所在内存的几十甚至几百分之一,
-
总结:采用字节码的编译执行方案,牺牲了执行速度,提升解析编译速度,保证速度差不多的同时,减少大量内存的占用
1.3 编译器(TurboFan)与热代码
- 接下来解决执行速度慢的问题。在 V8 模块中会有专门的监控模块,来监控同一代码是否多次被调用,如果被多次调用,那么就会被标记为热代码(HotSpot)
- 当存在热代码的时候,V8 会借着 TurboFan 将为热代码的字节码转为机器码并缓存下来,这样一来,当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。当然热代码相对来说还是少部分的,所以缓存也并不会占用太大内存,并且提升了执行效率,同样此处也是牺牲空间换时间。
- 如果热代码在某次执行的时候,突然其中的某个属性被修改了,那么编译成机器码的热代码会被退回到 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,其内部都会初始化三个属性
- 作用域链(scope chain)
- VO (Variable Object)
- this
2.2 分析阶段
-
分析阶段
- 创建分析对象: 用于处理执行时,访问变量和方法时候,根据一定规则进行作用域的访问。
- 预编译:提高运行时的效率,会把把代码进行预编译,如变量提升。
-
发生在何时
- 解析阶段
-
分类
- 代码执行之前
- 声明提升
- 函数声明整体提升
- 函数执行之前
- 创建一个AO
- 找形参和变量声明,作为AO的key,值为undefined
- 将实参与形参统一
- 在函数体内找函数声明,将函数名作为AO对象的key,值为函数体
- 全局
- 创建GO
- 找全局变量声明,作为GO的key,值为undefined
- 找全局函数声明,作为GO的key,值为函数体
- 代码执行之前
2.3 执行过程
- 创建执行上下文:根据分析的对象创建执行上下文
- 上下文入栈执行
- 变量赋值
- 方法调用
Author gsemir
LastMod 2022-08-15