前言
宏观视角下的浏览器
Chrome架构:仅仅打开了1个页面,为什么有4个进程?
进程与线程:
- 线程是不能单独存在的,它是由进程来启动和管理的(线程依附进程,多线程并行处理提高运算效率)
- 一个进程就是一个程序的运行实例
- 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
- 线程之间共享进程中的数据。
- 当一个进程关闭之后,操作系统会回收进程所占用的内存。
- 进程之间的内容相互隔离。
其实早在 2007 年之前,市面上浏览器都是单进程的,而单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里。单进程会造成以下几个问题:
- 不稳定(任何一个插件或模块挂了,整个浏览器就会崩溃)
- 不流畅(所有模块运行在同一个线程中,同一时刻只能有一个模块运行,若模块执行时间过长则页面会失去响应,变卡顿,或许还有内存泄露)
- 不安全(插件或脚本可能利用操作系统或浏览器的漏洞)
浏览器中js执行机制
V8工作原理
编译器和解释器:V8是如何执行一段JavaScript代码的?
- 静态语言,在使用之前就需要确认其变量数据类型的称为静态语言。
- 动态语言,在运行过程中需要检查数据类型的语言称为动态语言,比如js,因为在声明变量之前并不需要确定其数据类型。
- 弱类型语言,支持隐私类型转换的语言称为弱类型。
- 强类型语言,不支持隐私类型转换的语言称为强类型语言。
JavaScript 是一种弱类型的、动态的语言,意味着什么呢?
- 弱类型,意味着不需要告诉js引擎变量是什么类型,引擎在代码运行的时候会计算出来。
- 动态,意味着可以用同一个变量保存不同类型的数据。
之所以存在编译器和解释器,是因为机器不能直接理解我们所写的代码,所以在执行程序之前,需要将代码翻译成机器能懂的机器语言。按语言的执行流程分为编译型和解释性语言。
- 编译型语言,在执行程序之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样在每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。比如C/C++,GO等都是编译型语言。
- 解释性语言,在每次运行时都需要通过解释器对程序进行动态解释和执行。比如Python,JavaScript等。
V8 在执行过程中既有解释器 Ignition和编译器 TurboFan。
一、将源代码生成AST和执行上下文
- 无论解释性语言还是编译性语言,在编译过程中,都会生成AST。
- 编译器和解释器理解的只有AST。
- babel的原理是先将ES6转为AST,然后再将ES6的AST转为ES5的AST,最后ES5的AST生成js源代码。
- 生成AST需要先分词(将源码拆解为一个个的token),然后再词法分析(根据语法规则将token转为AST)
二、生成字节码
- 有了AST和执行上下文,解释器Ignition会根据AST生成字节码,并解释执行字节码
注意:其实一开始 V8 并没有字节码,而是直接将 AST 转换为机器码,由于执行机器码的效率是非常高效的,所以这种方式在发布后的一段时间内运行效果是非常好的。但是随着 Chrome 在手机上的广泛普及,特别是运行在 512M 内存的手机上,内存占用问题也暴露出来了,因为 V8 需要消耗大量的内存来存放转换后的机器码。为了解决内存占用问题,V8 团队大幅重构了引擎架构,引入字节码,并且抛弃了之前的编译器,最终花了将进四年的时间,实现了现在的这套架构。
机器码所占用的空间远远超过了字节码,所以使用字节码可以减少系统的内存使用
三、执行代码
生成字节码之后,接下来就要进入执行阶段了。
通常,如果有一段第一次执行的字节码,解释器 Ignition 会逐条解释执行。在执行字节码的过程中,如果发现有热点代码(HotSpot),比如一段代码被重复执行多次,这种就称为热点代码,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,这样就大大提升了代码的执行效率。
V8 的解释器和编译器的取名也很有意思。解释器 Ignition 是点火器的意思,编译器 TurboFan 是涡轮增压的意思,寓意着代码启动时通过点火器慢慢发动,一旦启动,涡轮增压介入,其执行效率随着执行时间越来越高效率,因为热点代码都被编译器 TurboFan 转换了机器码,直接执行机器码就省去了字节码“翻译”为机器码的过程。
其实字节码配合解释器和编译器是最近一段时间很火的技术,比如 Java 和 Python 的虚拟机也都是基于这种技术实现的,我们把这种技术称为即时编译JIT。。具体到 V8,就是指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
对于 JavaScript 工作引擎,除了 V8 使用了“字节码 +JIT”技术之外,苹果的 SquirrelFish Extreme 和 Mozilla 的 SpiderMonkey 也都使用了该技术。
JavaScript 的性能优化
在过去几年中,JavaScript 的性能得到了大幅提升,这得益于 V8 团队对解释器和编译器的不断改进和优化。
虽然在 V8 诞生之初,也出现过一系列针对 V8 而专门优化 JavaScript 性能的方案,比如隐藏类、内联缓存等概念都是那时候提出来的。不过随着 V8 的架构调整,你越来越不需要这些微优化策略了,相反,对于优化 JavaScript 执行效率,你应该将优化的中心聚焦在单次脚本的执行时间和脚本的网络下载上,主要关注以下三点内容:
- 提升单次脚本的执行速度,避免 JavaScript 的长任务霸占主线程,这样可以使得页面快速响应交互;
- 避免大的内联脚本,因为在解析 HTML 的过程中,解析和编译也会占用主线程;
- 减少 JavaScript 文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存。
编译器和解释器的相关概念和理论对于程序员来说至关重要,向上能让你充分理解一些前端应用的本质,向下能打开计算机编译原理的大门。通过这些知识的学习能让你将很多模糊的概念关联起来,使其变得更加清楚,从而拓宽视野,上升到更高的层次。