浅谈浏览器工作原理与V8引擎...
JavaScript 是一门高级的编程语言,那么相反就会存在低级编程语言,从编程语言发展历史来说,可以划分为三个阶段:
- 机器语言: 1000101000,一些机器指令
- 汇编语言: mov ax,bx ,一下汇编指令
- 高级语言: C、C++、Java、JavaScript、Python...
但是计算机本身是不认识高级语言的,所以我们的代码最终还是需要被转换成机器指令在计算机中执行。
浏览器的工作原理
JavaScript代码,在浏览器中是如何被执行的?
当我们向浏览器的搜索框中输入服务器的地址时,服务器会返回一个 index.html 页面,当我们在解析 HTML 页面的时候,如果遇到了 link 标签就下载对应的css文件,如果遇到了 script 标签就下载对应的js文件。
浏览器的内核:不同的浏览器由不同的内核组成,浏览器的内核指的就是浏览器的排版引擎,也成浏览器引擎、页面渲染引擎
Gecko: 早起被Netscape和Mozilla Firefox浏览器使用 Trident: 微软开发,被IE4~IE11浏览器使用,但是Edge浏览器已经转向Blink Webkit: 苹果基于KHTML开发,开源的,用于 Safari、Google Chrome之前也在使用 Blink: 是Webkit的一个分支,Google开发,目前应用于Google Chrome、Edge、Opera等... 等等...
浏览器渲染过程
当我们在执行以上过程时,在HTML解析的时候,就会按照规则生成对应的DOM树,当我们遇到了css的link标签就会按照css的规则进行解析css文件,如果在生成DOM树的过程中遇到了js代码,我们就会加载与执行js代码,然后将执行完毕的css规则与dom树进行结合,形成一个渲染树(Render Tree),生成之后就要进行布局操作,因为最终我们元素放在什么位置还需要根据浏览器的状态进行布局,布局完成就可以进行绘制了,最终展示绘制之后的结果。
那么js代码由谁去执行呢?
JavaScript引擎
为什么需要JS引擎呢?
我们前面说过,高级的编程语言都是需要转成最终的机器指令来执行的; 事实上我们编写的JS无论你交给浏览器或者Node执行,最终都是需要被CPU执行的; 但是CPU只认识自己的指令集,实际上是机器语言,才能被CPU所执行; 所以我们需要JS引擎帮助我们将JS代码翻译成CPU指令来执行。
V8引擎:Google开发的强大的JavaScript引擎,帮助Chrome从中多浏览器中脱颖而出。
V8引擎的原理
V8 引擎的解析图(官方)
逐级拆解①
上图中的 Parse 是很复杂的一个事件,包括 词法分析和语法分析 ,然后将其转换为 AST抽象语法树。
词法分析:就是将程序源代码分解成对编程语言来说有意义的代码块,这些代码块被称为词法单元(token)
实例:const name = 'joyr' 会被如何分解?
我们从上图可以看到,这段代码被分解出来了四个token:
const 关键字
"type": "Keyword" => 关键字 "value": "const" => const
name 标志符
"type": "Identifier" => 标志符 "value": "name" => name
= 运算符
"type": "Punctuator" => 标点符号 "value": "=" => =
joy 数值
"type": "String" => 字符串 "value": "'joy'" => joy
语法分析(Parser) :Parser 是V8的解析器,负责根据生成的 tokens 进行语法分析
PreParser:预解析,又称为延迟解析,它只解析未被立即执行的代码(如函数),不生成 AST ,只确定作用域,以此来提高性能,当预解析后的代码开始执行时,才进行 Parser 解析。 什么是预解析? 看如下代码:
function foo () {
function bar() {
}
}
foo()
在上述代码中,如果我们使用了 Parser ,会生成 foo 函数和 bar 函数的AST,但是 bar 函数并没有被调用,所以生成的 bar 函数的AST实际上是没有任何意义的,这就可以用到 PreParser了。再看如下代码:
function outer () {
function inner () {
}
}
(function bar () {
})()
outer()
① 当V8引擎遇到 outer 函数与 inner 函数声明时,发现它未被立即执行,就采用 PreParser 对其进行预解析 ② 当 V8 遇到了(bar( ))( ) 时,就会知道这是一个立即执行表达式,所以就采用 PreParser 对其解析 ③ 当 outer( ) 函数被调用的时候,会采用 Parser 对 foo 函数进行解析,此时 inner 函数会再次进行预解析,以此可以得出如果函数的嵌套层级过深,就会导致执行多次预解析,从而影响性能.
Parser 的主要任务:① 分析语法错误 ② 输出AST抽象语法树 ③ 确定词法作用域
来看上述代码生成的 AST 抽象语法树:
逐级拆解②
经过了词法分析与语法分析就可以拿到抽象语法树,有了抽象语法树之后就可以转换成es5的代码,也可以通过 Ignition 这个库将其转换成 bytecode 字节码文件。
那为什么不直接将 AST 转换成可以直接在计算机中执行的二进制代码呢? 因为 JS 代码不知道会运行在什么环境上面,而不同的环境却拥有不同的CPU,不同的CPU架构所指定的机器指令是不同的,所以就直接转换成了字节码,可以跨平台,在各个系统中都可以转换成相对应平台的CPU指令。
逐级拆解③
现在来分析一个问题:如果我每次想执行代码都需要将字节码转换成想对应的二进制代码,这是一件很浪费性能和时间的事。
有了这个思考,我们就可以尝试将字节码直接变成对应平台的二进制代码: 假设我们定义一个函数:
function foo() {
console.log('joyr')
}
这段代码在程序中是一成不变的,如果我们需要重复执行多次,直接将其转换成对应的机器码,我们就可以省去之前的转码过程,性能肯定是提升的。
TurboFan库:可以通过 Ignition 进行收集信息,将执行频率较高的函数标记成 hot => 即热函数,我们就可以将其转换成对应的机器码直接进行执行。
我们再次定义一个函数:
function sum (num1, num2){
return num1 + num2
}
我们进行调用:
sum(500, 20) => 520
sum(1000, 314) => 1314
这种情况下是没有任何问题的,但是因为js不存在类型检测,我们像下面传入参数:
sum('hello', 'world') => helloworld
这时候显然我们使用之前的机器码的本意并不是进行字符串拼接,所以我们在执行对应的 CPU 指令的时候,就无法使用,然而在 V8 引擎中存在 Deoptimization ,一旦我发现我在执行已经就绪的机器指令的时候,操作发生了变化,它就会将这次对应的机器指令回头转换成字节码再运行。
经过了上述陈述,我们可以发现,如果我们在函数中传递参数的时候保证了类型,对性能的提升是很有帮助的!(所以 ts => js 是可以很有效的提升性能的,因为 ts 存在类型限制)
以上就是 V8 引擎的概述!