1.V8
编译器有 SpiderMonkey、JavascriptCore、V8 V8 是 Google 开源的 JavaScript 引擎,被广泛应用于各种 JavaScript 执行环境,比如 Chrome 浏览器、Node.js、Electron 以及 Deno 等。
1.1 V8的执行流程
- 解析器:将Javascript源码通过解析器转化为AST语法树🌲
- 解释器:将AST通过解释器翻译为字节码,一边解释一遍执行
- 优化编译器:在解释执行的过程中,解释器会记特定代码片段的运行次数,如果运行次数超过了某个阈值,那么该段代码会被标记为热代码(hot code)并且将运行信息反馈给优化编译器,优化编译器根据反馈信息优化编译字节码最终生成优化后的机器码,这样当该段代码再次执行时,解释器就直接使用优化后的机器码执行,不用再次解释,从而提高代码的运行效率。
1.2 解析器(Parser)
解析器的过程可以分为两个部分:词法分析和语法分析
- 词法分析:将字符流转化为tokens。字符流就是我们编写的一行行代码,token是指语法上不能再分割的最小单位。
- 语法分析:根据语法规则将tokens组成一个嵌套层级的抽象语法树(AST),在这个过程中如果源码不符合规范,解析过程就会终止,并抛出语法错误。
举个例子:var a = 1; 对于这个代码
首先词法分析阶段会将这个字符流转化为5个token
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "a"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "Numeric",
"value": "1"
},
{
"type": "Punctuator",
"value": ";"
}
]
然后通过语法分析通过tokens生成AST。(可以通过astexplorer.net/ 这个网站查看)
{
"type": "Program",
"start": 0,
"end": 10,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 10,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 1,
"raw": "1"
}
}
],
"kind": "var"
}
],
"sourceType": "module"
}
注意: 如果对于所有的js源码都要完全经过解析才能执行,那必然面临3个问题:
- 代码执行时间变长
- 内存消耗增加
- 占用磁盘空间
所以现在的引擎都会进行延迟解析。举个例子
·11·1·1·11···qaqqqq1`wq2frdeswaq
function foo(a,b){
var res = a + b
return res
}
var a = 1;
var c = 2;
foo(1, 2)
V8解析器会从上往下解析代码,当V8解析到foo的时候发现它不是立即执行,那么只会解析函数声明,并不会解析函数内部的代码。只有当执行到 foo(1,2)的时候才会去执行函数内部的代码
2. 垃圾回收
2.1 引用计数法
跟踪记录每一个变量值被使用的次数。
当一个变量被一个引用类型的值赋值后,那么这个值的引用次数就 +1 ,如果这个变量被其他的值赋值后,那么上次的值的引用次数就-1。
举个例子:var a = {x: 1},那么{x:1}的引用次数就为1.然后又执行了a = {x:2},那么{x:1}的次数-1就变成了0。它就被立即回收
优点:可以被立即回收。
缺点:需要一个计数器,要占用很大位置。还有就是无法解决循环引用无法回收的问题。
2.2 标记清除
执行过程:
- 垃圾收集器会在运行时给所有的变量都加上一个标记,假设内存中的所有对象都是垃圾,标记为0
- 然后从根对象开始遍历,把不是垃圾的节点改成1
- 清理所有的标记为0的垃圾,回收内存空间
- 把所有的内存中的对象标记修改为0,等待下一次回收
优点:只有一个优点就是实现比较简单,无非就是标记或者不标记。
缺点:清除内存之后,剩余的内存位置是不变的,就导致内存空间不连续,出现内存碎片,如果清理之后,再去重新分配对象内存空间则需要遍历所有的内存块。
2.3 优化方案:分代式垃圾回收机制
针对垃圾回收的缺点,V8 通过分代式的方法去管理内存。会分为新生代和老生代
新生代:当有新的对象时,会将其分配到使用区,如果使用区快满的时候,开始垃圾清除对于仍然保留在内存中的对象将其复制到空闲区,然后将使用区和空闲区互换。
如果一个对象在多次复制的过程中一直保留,则会直接将其移动到老生代中进行管理。
另外,如果一个对象复制到空闲区时,如果空闲区的占用空间超过25%,那么这个对象会直接移入老生代中。
老生代:中的垃圾回收会通过标记整理的方法进行处理。
问题:JS是单线程语言,在执行垃圾回收的时候,会阻塞JS的执行。
V8的优化方式就是并行回收,就是通过开启辅助线程进行垃圾回收,主线程则用来执行JS。
但是这样依然会阻塞。
继续优化:切片标记:将一次GC标记的过程分成很多小步,每执行一小步就继续执行JS,这样最大程度的保证JS的执行,也不会有较长时间的阻塞
3.运行机制
3.1 浏览器的进程
浏览器有五大进程:主进程、插件进程、渲染进程、网络进程、GPU进程
主进程:负责界面显示、用户交互、子进程管理插件进程:负责插件进程,当插件崩溃时不会影响浏览器页面渲染进程:将HTML、css、js转化为用户可以与之交互的页面。我们通常所说的JS引擎和排版内核都是运行在这个进程中的网络进程:负责网络资源的加载GPU进程:负责3D效果的渲染
3.2 浏览器的事件循环
JS是单线程的语言。但是它为什么是单线程呢?
假如它是多线程的。一个线程在某个DOM节点上添加了内容,而另一个线程又删除了节点。那这时该以哪个线程为准,所以为了避免复杂性,JS就只能是单线程的。
单线程也就意味着所有的任务都需要排队,前一个任务结束才会执行下一个任务。
在实际的执行过程中,就必须要按照顺序一步一步执行吗?即使中间的某一步可能会花费很长的时间。比如网络请求数据
答案当然是否定的。任务可以分为两种: 同步任务,异步任务