上个章节我们讲到了 V8 是一个由 Google 开发的开源 JavaScript 引擎,目前用在 Chrome 浏览器和 Node.js 中,其核心功能是执行易于人类理解的 JavaScript 代码。
那么 V8 又是怎么执行 JavaScript 代码的呢?
讲述这块内容的过程可能会略微有点枯燥,但是懂得整个执行过程更加利于你写出更高执行效率的代码,懂得未来JavaScript发展趋势,更能加深对JavaScript的理解。
V8 在执行 JavaScript 代码过程中其主要核心流程分为编译和执行两步。首先需要将 JavaScript 代码转换为低级中间代码或者机器能够理解的机器代码,然后再执行转换后的代码并输出执行结果。
有同学可能会问,JavaScript源代码不转化成机器代码难道就不能执行了吗?
答案是yes, 我们所编写的JavaScript代码依赖于宿主浏览器加载进内存,最后由CPU去执行。而 CPU 只能识别二进制的指令,但是对程序员来说,二进制代码阅读、书写难度超级大。 所以将实现各种功能的二进制指令集转化为人类可以识别和记忆的符号的汇编语言就出现了。
1000100111011000 机器指令
mov ax,bx 汇编指令
“1000100111011000” 这是二进制指令,意思是将一个寄存器中的数据移动到另外一个寄存器中。 下面是汇编语言实现相同的功能,可以明显感受到容易理解多了。
但是如果要汇编语言来实现一个功能,那么你需要为每种架构的 CPU 编写特定的汇编代码,还需要了解和处理器架构相关的硬件知识,比如寄存器、内存、操作 CPU 等。
需要为每种架构的CPU编写特定的汇编代码
因此我们需要一种屏蔽了计算机架构细节的语言,能适应多种不同 CPU 架构的语言,能专心处理业务逻辑的语言,诸如 C、C++、Java、C#、Python、JavaScript 等,这些“高级语言”就应运而生了。和汇编语言一样,处理器也不能直接识别由高级语言所编写的代码,那怎么办呢?
通常,要有两种方式来执行这些代码。
第一种是解释执行,需要先将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释执行中间代码,然后直接输出结果。具体流程如下图所示:
第二种是编译执行。采用这种方式时,也需要先将源代码转换为中间代码,然后我们的编译器再将中间代码编译成机器代码。通常编译成的机器代码是以二进制文件形式存储的,需要执行这段程序的时候直接执行二进制文件就可以了。还可以使用虚拟机将编译后的机器代码保存在内存中,然后直接执行内存中的二进制代码。
V8 在 2008 年刚推出的时候,它选择的就是这种方式提供了一个快速编译成机器码的编译器,虽然没做太多优化,但性能已经是当时其他 JavaScript 引擎的 10 倍了。对 JavaScript 编译器来说,它最大的挑战就在于,当我们打开一个页面的时候,源代码的下载、解析(Parse)、编译(Compile)和执行,都要在很短的时间内完成,否则就会影响到用户的体验。 而V8引擎的优秀性能也带动着chrome浏览器的迅速崛起。
目前V8的架构跟最初发生了巨大的变化,它并没有采用某种单一的技术,而是混合编译执行和解释执行这两种手段,我们把这种混合使用编译器和解释器的技术称为 JIT(Just In Time)技术。
这是一种权衡策略,因为这两种方法都各自有各自的优缺点,解释执行的启动速度快,但是执行时的速度慢,而编译执行的启动速度慢,但是执行时的速度快。
我们先看上图中的最左边的部分,在 V8 启动执行 JavaScript 之前,它还需要准备执行 JavaScript 时所需要的一些基础环境,这些基础环境包括了“堆空间”“栈空间”“全局执行上下文”“全局作用域”“消息循环系统”“内置函数”等,这些内容都是在执行 JavaScript 过程中需要使用到的。
基础环境准备好之后,接下来就可以向 V8 提交要执行的 JavaScript 代码了。首先 V8 会接收到要执行的 JavaScript 源代码,不过这对 V8 来说只是一堆字符串,V8 并不能直接理解这段字符串的含义,它需要结构化这段字符串。结构化,是指信息经过分析后可分解成多个互相关联的组成部分。 (用白话讲就是进行词法分析 语法分析)。
V8 源代码的结构化之后,就生成了抽象语法树 (AST),我们称为 AST,AST 是便于 V8 理解的结构。
有了 AST 和作用域之后,接下来就可以生成字节码了,字节码是介于 AST 和机器代码的中间代码。但是与特定类型的机器代码无关,解释器可以直接解释执行字节码,或者通过编译器将其编译为二进制的机器代码再执行。
生成了字节码之后,解释器就登场了,它会按照顺序解释执行字节码,并输出执行结果。相信你注意到了,在解释器附近画了个监控机器人,这是一个监控解释器执行状态的模块,在解释执行字节码的过程中,如果发现了某一段代码会被重复多次执行,那么监控机器人就会将这段代码标记为热点代码。
当某段代码被标记为热点代码后,V8 就会将这段字节码丢给优化编译器,优化编译器会在后台将字节码编译为二进制代码,然后再对编译后的二进制代码执行优化操作,如果再执行到这段代码时,那么 V8 会优先选择优化之后的二进制代码由处理器直接执行。这样代码的执行速度就会大幅提升。
不过,和静态语言不同的是,JavaScript 是一种非常灵活的动态语言,对象的结构和属性是可以在运行时任意修改的,而经过优化编译器优化过的代码只能针对某种固定的结构,一旦在执行过程中,对象的结构被动态修改了,那么优化之后的代码势必会变成无效的代码,这时候优化编译器就需要执行反优化操作(也有叫去优化操作),经过反优化的代码,下次执行时就会回退到解释器解释执行。
所以你在复杂的项目中你要提升JavaScript的执行效率跟性能,应该减少反优化。 总结成一句话:JavaScript引擎它喜欢你对象的结构越稳定越好,如果你把对象的结构不停的在改变的话对它而言可优化性就变低了。
隐藏类 内联缓存
那怎样才能减少反优化具体有哪些例子:
1:尽量在构造函数中声明对象属性
2:尽量保持对象的稳定性
3:多次调用某个函数时,尽量保持参数类型一致。
4:**不要在函数中声明类**