视野前端(二)V8引擎是如何工作的
许多同学在阅读了基础进阶系列文章之后,对JS代码的执行顺序理解得更清晰了。可也有不少好学的大佬在此基础上进一步思考,JS引擎到底是如何工作的?什么时候解析?什么时候执行?特别是在其他地方阅读了不少各种说法的文章之后,疑惑更重了。
这里就以V8引擎为例,跟大家聊一聊,JS引擎是如何工作的。
JS引擎是一个应用程序,它是浏览器引擎的一部分。每个浏览器的JS引擎都不一样。例如chrome的V8,firefox的SpiderMonkey,Safari的Nitro等等。
所有的JS引擎原则上都会按照ECMAScript标准来实现。因此大家的实现方式可能有所差异,解析原理也不尽相同,但大体表现基本上能保持一致。想要了解JS引擎的工作思路,了解V8就足够了。
Chrome(还有Nodejs)的JS引擎是V8,他的内部有许多小的子模块组成。这里我们只需要了解其中最常用的四个模块即可。
1.parser
顾名思义。这个模块的作用是将我们自己编写的JS源码,转换为抽象语法树(Abstract Syntax Tree)。在许多其他文章里,提到的词法语法分析过程,就是 parser
来完成。
我们可以通过在线网站 https://esprima.org/demo/parse.html# 来观察我们的代码通过词法分析变成AST之后大概会是神马样子。
从该工具中,我们还发现一个在介绍词法分析过程的文章里经常提到的一个东西: Token
token: 词义单位,是指语法上不能再分割的最小单位,可能是单个字符,也可能是一个字符串。
工具中使用如下的方式来表示多个tokens
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "a"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "10"
},
{
"type": "Punctuator",
"value": ","
},
{
"type": "Identifier",
"value": "b"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "20"
}
]
那么,parser模块的工作过程,就比较明了了。大致如下:
此图仅为大致过程,例如官方文档中提到的,tokens的过程具体是由一个名为scanner的扫描工具来完成。
我们知道,声明多个连续的变量时,可以只使用一个关键字,如下:
var a = 10, b = 20
这种方式比多个变量各自声明性能上会更好一点,为什么? 利用工具,观察一下两种方式下tokens和AST的不同,就能马上明白了。
那么问题来了,在这个过程中,执行上下文创建了没有?
其实还没有,我们的代码在这个阶段,还没有正式进入运行。
所以留一个简单的问题,如下的代码,直接执行,在这个阶段会直接报错吗?
如果有兴趣,在评论里留下你的答案与分析。
var a = b;
1.Ignition
在v8文档中可以得知,Ignition是V8提供的一个解释器。他的作用是负责将抽象语法树AST转换为字节码。并同时收集下一个阶段(编译)所需要的信息。这个过程,我们也可以理解为预编译过程。
在之前我对变量对象的介绍中,曾经用下面的方式表达执行上下文的生命周期。这里预编译过程,其实就是执行上下文的第一个阶段。如图所示:
因为基于性能优化的考虑,预编译过程与真正的编译有的时候不会区分的那么明确,有的代码在预编译阶段就能直接执行。
1.TurboFan
V8引擎的编译器模块。利用Ignition收集到的信息,将字节码转换为汇编代码。 这也是我们之前提到过的可执行代码的执行阶段。
当然,到这里,如果不是对V8特别感兴趣的话,就不必在继续深究具体的细节了。基本上JS代码的执行过程都相对清晰。
官方文档中,我们可以查阅一个讲述V8引擎优化过程[1]的一个PPT,可以发现,在不同的版本中,解释器与编译器的交互过程每个版本都在变化。
这里截取了一些图展示编译过程的演变,PPT里面还有很多更详细的介绍,如果感兴趣的同学可以阅读PPT做更深入的了解。
为了达到更好的性能,执行过程并非严格按照先由解释器解析,然后交给编译器编译的定式执行。JS作为解释型的动态语言,在整个解析编译的过程中,就有许多优化的空间。例如我们常常听到的JIT模式。
我们自己也能够猜到一些优化的点:
例如,如果一个函数不被调用,我们可以不用去编译它。
一个函数被调用很多次,那么我们可以想办法给他标记上,只需要编译一次等等。
1.Orinoco
垃圾回收模块。
Orinoco也是使用我们熟知的标记清除法来进行垃圾回收。
当执行上下文创建时,变量进入该环境,我们就可以对该变量对应的内存进行标记。如果执行上下文执行完毕,这个时候,就可以将所有进入该环境的变量标记为可清除状态。我们通俗的说法就是,当一份内存失去了引用,那么它就会被垃圾回收工具回收。
不过还有两个需要注意的地方。
一个是全局上下文。在程序结束之前,全局上下文始终存在。通常来说,JS程序运行期间,全局上下文不会有执行结束的时间节点。因此定义在全局上下文的状态永远都不会被标记。除非我们手动将变量设置为null,它对应的内存都不会被回收。
另外一个是闭包。因为闭包的特性是能够始终保持内存的引用。因此当我们希望利用闭包的特性达到某些目的时,即使它对应的执行上下文已经执行完毕了,我们也会想办法让内存的引用始终保持。
References
[1]
V8引擎优化过程: https://docs.google.com/presentation/d/1chhN90uB8yPaIhx_h2M3lPyxPgdPmkADqSNAoXYQiVE/edit#slide=id.g1357e6d1a4_0_58
- 谈谈分布式事务之二:基于DTC的分布式事务管理模型[上篇]
- 学习SpringMVC——拦截器
- 学习SpringMVC——国际化+上传+下载
- 行业研究:大数据(一)
- 控制并发访问的三道屏障: WCF限流(Throttling)体系探秘[下篇]
- 如何通过VPC在本机搭建局域网
- 你常用的10个MySQL命令
- WCF技术剖析之三十一: WCF事务编程[下篇]
- WCF技术剖析之三十一:WCF事务编程[上篇]
- 学习SpringMVC——你们要的REST风格的CRUD来了
- 并发中的同步--WCF并发体系的同步机制实现
- WCF 技术剖析之三十三:你是否了解WCF事务框架体系内部的工作机制?[下篇]
- 学习SpringMVC——从HelloWorld开始
- 小程序年底重磅更新,小游戏上线,最强入口也来了!
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- python和js交互调用的方法
- Python中flatten( ),matrix.A用法说明
- python中id函数运行方式
- CentOS 7如何实现定时执行python脚本
- PHP自动生成缩略图函数的源码示例
- 解决tensorflow 释放图,删除变量问题
- php生成word并下载代码实例
- TensorFlow保存TensorBoard图像操作
- 浅谈PHP SHA1withRSA加密生成签名及验签
- PHP PDO数据库操作预处理与注意事项
- laravel 框架配置404等异常页面
- Django –Xadmin 判断登录者身份实例
- Laravel 队列使用的实现
- keras 两种训练模型方式详解fit和fit_generator(节省内存)
- Keras 中Leaky ReLU等高级激活函数的用法