本文搬运的是我自己的博客,有兴趣可以移步至:浏览器工作原理与V8引擎
1.浏览器中JS是如何被执行的
当我们在输入一个域名之后会现经历DNS服务器解析变成一个ip地址,然后浏览器会通过这个ip地址去访问服务器,服务器会放回一个index.html页面,浏览器开始解析这个inde.html文件,当解析到link标签时就开始下载对应的css文件,遇见script标签就开始下载对应的js文件。
2.浏览器内核
浏览器对各种html、css、js文件的解析都需要通过浏览器内核来完成,它是持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是JS引擎。渲染引擎在不同的浏览器中也不是都相同的。下面是一些主流浏览器内核:
- Gecko:早期被NetScape和Mozilla Firefox浏览器使用;
- Trident:IE4-IE11浏览器内核,现在的Edge以转向了Blink;
- Webkit:苹果基于KHTML开发的、开源的,用于Safari,Google Chrome也曾使用过;
- Blink:是Webkit的一个分支,Google开发,目前用于Google Chrome、Edge、Opera等浏览器。
3.浏览器渲染过程
浏览器在渲染一个网页时会先收到一个服务器返回的index.html文件,然后浏览器通过渲染引擎和JS引擎对整个index.html进行解析,HTML标签会被HTML Paeser解析成DOM Tree(DOM树),在生成DOM树的过程中会有一些JS代码来对DOM进行操作,这些JS代码由JS引擎解析执行(后面会对JS引擎做介绍),CSS文件会被CSS Parser解析成style Rules(样式规则),DOM Tree和style Rules通过Attachment附加到一起,再结合Layout(布局引擎)进行布局(虽然前面已经将DOM树和样式结合形成了渲染树,但随着浏览器所处的状态不同,布局也会随之改变,所以需要通过Layout布局引擎布局),这会生成一个Render Tree,然后就可以开始绘制(Painting),最后进行展示(Display)。
4.JavaScript引擎
- JavaScript是一门高级语言,高级语言都是需要编译成01的机器语言来执行的;
- 无论是浏览器还是Node执行JS代码,最终都会交个CPU执行,CPU只能读懂机器语言;
- JavaScript引擎做用就是将JS转译成机器指令交个CPU来执行。
####常见的JavaScript引擎:
- **SpiderMonkey(奥丁猴):**火狐浏览器使用的JS引擎,它是第一款JS引擎,由Brendan Eich开发(JS作者);
- **Chakra(查克拉):**微软开发的JS引擎,用于IE和Edge浏览器;
- **JavaScriptCore:**用于Safari浏览器,是苹果公司开发的的JS引擎;
- **V8:**用于Chrome,是谷歌开发的引擎,在Window上是最强的JS引擎,Node也是基于V8引擎开发的。
####浏览器内核与JS引擎的关系:
浏览器内核包含了JS引擎,以 WebKit 内核为例,WebKit 分为两个部分:
- **WebCore:**负责HTML解析、布局、渲染等工作;
- **JavaScriptCore:**解析、执行JS代码。
5.V8引擎执行JS过程
- V8引擎是用 C++ 编写的Google开源的高性能 JavaScript 和 WebAssembly 引擎,目前它主要用于Chrome和Node.js等;
- 它实现了 ECMAScript 和 WebAssembly,并在Windows7或更高版本,macOS10.12+和使用x64、AI-32、ARM或MIPS处理器的Linux系统上运行;
- V8可以独立运行,也可以嵌入到认可C++应用程序中。
V8引擎解析和运行JS代码流程:
V8引擎会先对Js源代码做一个解析(Parse),分为词法分析和语法分析,解析完之后会生成一个抽象语法树(AST),AST 通过V8的一个库 Ignition转换为字节码,之所以转换成字节码而不是机器码,是因为在不同的环境中cpu的指令集是不同的,就像Java通过JVM实现跨平台一样,这里的字节码也起到了类似的作用,字节码不依赖于平台,它在任何平台都可以运行。当需要运行时V8引擎会将字节码转译成对应系统的机器码运行,相交于直接将AST运行转译为对应的机器码,这样的效率高很多。但是每一次运行都需要通过字节码编译一遍,在面对高频使用的的一些函数、变量、类等js代码时,相同的字节码编译多次非常浪费性能。V8引擎有一个TurboFan库,它可以通过Ignition库分析可能高频率使用的一些变量、函数等代码,给他们打上一个hot标记,这些被打上hot标记的函数、变量等代码会被TurboFan转换为优化的机械码,等到后面调用时就直接运行被优化的机械码,不需要在去编译字节码。 如果被优化编译的函数、变量等出现了某些改变需要重新编译,那么对应的机械码会反向优化(Deoptimization)转回字节码,再由字节码文件完成编译运行。
6.V8引擎解析细节
下面是V8官方所提供的JS解析流程:
可以看到解析流程是从Blink内核开始解析HTML开始的,Blink后面那些就是V8引擎,在解析HTML的过程中,内核将加载的JS代码以流的方式交给V8引擎进行解析,V8引擎首先会通过Stream对JS代码的编码进行转化然后交给Scanner(扫描器),扫描器会对代码进行词法分析转换成tokens,tokens是一个对象数组。将tokens传递个Parse做语法分析,最后生成AST(抽象语法树),AST会被Ignition库转译成字节码最后编译成机器码运行。在tokens做语法分析的过程中,有一些并非立即会使用到的JS函数、对象等代码不会被立即被解析成AST,而是先进行预解析(PreParser)。例如下面代码:
function frist(){
function secnod(){
//...代码块
}
//....代码块
}
frist();
frist()函数会立即执行,secnod()从来没有运行过。如果没有预解析(PreParser),V8引擎在解析过程中会对frist()和secnod()都做解析。但secnod()从未运行过,所以secnod()这段代码没有必要转成AST结构,会浪费性能。于是V8引擎实现了延迟解析(Lazy Parsing)方案,也就是PreParser,作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析(Parser)是在函数被调用时才会进行。
在学习函数执行与作用域链之前先要了解一个底层概念:代码执行是先从磁盘加载到内存中,然后代码内存中被转换成机器指令,最后交给cpu执行。