一、认识JS引擎
1、为什么需要JS引擎?
越高级的编程语言,其实就是越接近人类思维的语言,但是这类语言一般电脑是完全不懂的,电脑能接受的只有包含0和1的机器指令,所以高级的编程语言,最终都要转成机器指令来执行。
无论是将js代码交给浏览器还是node执行,最后还是交给CPU执行,但是CPU只能认识自己的指令集,也就是包含0和1的机器语言,这个时候就需要JavaScript引擎来帮助我们将JavaScript代码翻译成CPU指令来执行。
2、常见的JavaScript引擎
SpiderMonkey:第一款JavaScript引擎,是JavaScript作者开发的
Chakra:微软开发的,用于IE浏览器
JavaScriptCore: webKit中内置的JavaScript引擎,Apple公司开发的
V8:Google开发的强大的JavaScript引擎,也帮助Chrome在众多浏览器中脱颖而出
3、浏览器内核和JS引擎的关系
以webkit为例:
二、V8引擎
1、V8引擎的原理
V8引擎是用C++编写的Google开源高性能的JavaScript和WebAssembly引擎,用于Chrome和Node.js等。
它能够实现ECMAScript和WebAssembly,并在Windows7或者更高版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux系统上运行。
V8引擎可以独立运行,也可以嵌入到任何C++应用程序中运行。例如,可以将V8引擎中使用Node.js看做是将将V8引擎嵌入到了应用程序中,那么Node.js就具备了执行JavaScript代码的能力。
原理图:
①、Parse模块会将JavaScript代码转换成AST,这是因为解释器并不直接认识JavaScript代码。如果函数没有被调用,是不会被转换为AST的
②、Ignition是一个解释器,会将AST转换为ByteCode。同时会收集TurboFan优化所需的信息(比如函数参数的类型信息,有了类型才能真实的运算)。如果函数只调用一次,Ignition将AST转换为ByteCode
③、为什么最后转化为字节码,而不是直接转化为机器码?
因为JS代码在什么样的环境下执行并不固定,有可能是使用Windows环境、或者是mac环境、或者是Linux环境的浏览器上,也可能是在Node.js中,环境不固定,不同环境中就会有不同的CPU,不同的CPU拥有不同的CPU架构,不同的架构能够执行的机器指令是不一样的。
转化为V8引擎规定好的字节码,不管在什么环境下都可以执行,是跨平台的,最后V8引擎会把字节码转化汇编指令,再转化为不同环境对应的CPU指令。
但是每次都走这套流程,还是不够方便。比如有一个函数是重复使用的,但是使用前面一套流程,每次使用这个函数的时候,都需要被转化为字节码,然后再变为CPU指令,性能比较低,如果可以直接将这个函数变为机器指令保存下来,使用这个函数的时候,直接运行机器指令,性能比较高,但是如果这个函数只运行一次,就没有必要转化变为机器代码保存下来,会浪费空间。
④、使用TurboFan库,是一个编译器,会将字节码编译为CPU可以直接执行的机器码,他可以利用ignition来收集函数的执行信息,了解到哪些函数执行次数比较多,会将这类函数标记为hot ,热函数,然后就会将这个函数转换为优化之后的机器指令,以后再使用这个热函数的时候,不需要上面繁琐的过程,直接执行机器指令就行。
但是实际上机器码也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生改变,之前优化的机器码并不能正确地处理运算,就会逆向的转换为字节码。
⑤、Deoptimization: 比如有一个函数
function sum(num1,num2){
num1+num2
}
调用sum函数
sum(20,30)
sum(28,30)
如果传入数字,调用sum函数,需要做的工作就是对两个数字进行相加,执行的机器指令永远是对这两个数字进行相加.
一旦改变传入值的类型,如果变成字符串,那么这个函数的意思就是两个字符串拼接。
sum("aaa","bbb")
这两种类型的传入值执行“+”操作对应的机器指令是不同的,JavaScript是不会对传入值的类型做检测的,那么还是使用数字相加的机器指令,这次函数调用的结果是不能够使用的。
但是V8引擎中提供了一种解决办法Deoptimization过程,这个过程是,一旦发现在执行机器指令时候,执行的操作不一样的时候,Deoptimization会反向优化,又转化为字节码,执行后续操作。
2、V8引擎的解析图
V8执行的细节:
①、Blink将源码交给V8引擎,Stream获取到源码并且进行编码转换
②、scanner会进行词法分析,词法分析之后会将代码转换为成tokens
③、tokens会被转换为AST树,经过Parser和PreParser:
Parser就是直接将tokens转换为AST树架构;
PreParser预解析,为什么会需要预解析?
1)如上图中的函数outer(),内部有一个函数inner(),但是并没有任何调用inner()的代码,那么就意味着并不是所有的JavaScript代码,都是一开始就被执行。对所有的JavaScript代码进行解析,必定会影响网页的运行效率。
2)V8引擎实现了Lazy Parsing(延迟解析)的方案,作用是将不必要的函数进行预解析,我只需要知道有这么个函数就行,也就是只解析暂时需要的内容,对函数的全量解析在函数被调用的时候才会执行。
3)例如上图中函数outer中的inner函数,它就是会执行预解析。
④、生成AST树之后,会被Ignition转成字节码,之后的过程就是代码的执行过程