浏览器原理-v8引擎-js执行原理
本文主要介绍浏览器原理-v8引擎-js执行原理,会涉及六个知识点
- 为什么JavaScript如此重要
- 浏览器从下载到渲染整个流程
- JavaScript引擎和浏览器内核关系
- V8引擎执行JavaScript代码的过程
- V8引擎的简单总结
- 全局代码执行和作用域提升
JavaScript 的重要性
- JavaScript 是前端的地基
- 前端行业近几年的快速发展,并且开发模式、框架越来越丰富。但是不管你使用的是Vue、React、Angular、包括Jquery,以及一些新出的框架。它们本身都是基于JavaScript开发的,所以再使用它们的过程中需要打好JavaScript的地基
- JavaScript 在工作中至关重要
- 在工作无论你使用什么样的技术,比如Vue、React、Angular、uniapp、ReactNative。也无论你做什么平台应用,比如PC、WEB、移动端WEB、小程序、公众号、移动端APP。它们都离不开JavaScript
- 前端的未来依然是 JavaScript
- 在可预见的前端未来中,我们依然是离不开JavaScript的。目前前端快速发展,无论是框架还是构建工具。而且框架也会进行不断更新,比如Vue3、React18、Vite2、TypeScript4.x。
-
著名的
Atwood
定律Stack Overflow
的创立人之一(Jeff Atwood)在 2007 年提出了著名的Atwood
定律- Any application that can be written in JavaScript, will eventually by written in JavaScript(任何可以使用
JavaScript
来实现的应用最终都会使用JavaScript
)
TypeScript 会取代 JavaScript 么
- TypeScript 只是给 JavaScript带来了类型思维
- 因为
JavaScript
本身长期没有对变量、函数参数等类型进行限制 - 这个可能给我们的项目带来某种安全隐患
- 因为
- 在之后的 JavaScript 社区中出现了一些列的类型约束方案
- 2014 年,
Fackbool
推出flow
来对JavaScript进行类型检查 - 同年,
Microsoft
微软也推出了TypeScript1.0
版本 - 他们都致力于为JavaScript提供类型检查,而不是取代JavaScript
- 2014 年,
- 并且在TypeScript的官方文档有这么一句话:源于JavScript,归于JavaScript
- TypeScript只是在JavaScript的一个超集,在它的基础之上进行了扩展
- 并且最终TypeScript还是需要转换成JavaScript
JavaScript是一门编程语言
- 为什么这里要强调JavaScript是一门编程语言呢?
- 事实上我们可以使用更加准确的描述:JavaScript是一门高级编程语言
- 那么高级编程语言,就有低级编程语言,从编程语言发展历史来说,可以划分为三个阶段
- 机器语言: 1000000101010101010,一些机器指令
- 汇编语言: mov、ax、bx,一些汇编命令
- 高级语言: C、C++、Java、JavaScript、Python
- 但是计算及它本身是不认识这些高级语言的,所以我们的代码最终还是需要被转换成机器指令
浏览器从下载到渲染整个流程
/Users/weihuijie/Desktop/WechatIMG28732.png
- 处理用户输入 当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字的类型:
- 搜索内容。地址栏会使用浏览器默认的搜索引擎进行页面跳转并搜索
- 请求的 URL 。比如输入的是 baidu.com,那么地址栏会根据规则,把这段内容加上协议,合成为完整的 URL,如 https://baidu.com。
当判断为请求URL的适合,当前页面就要被替换成新的页面,不过替换之前,浏览器还会给当前页面执行 beforeunload 事件(如果有的话)。当前页面没有监听 beforeunload 事件或者同意了继续后续流程,那么浏览器便进入loading状态
当浏览器开始加载一个地址之后,标签页上的图标便进入了加载状态。但此时页面显示的依然是之前打开的页面内容,并没立即替换为淘宝的页面。因为需要等待提交文档阶段,页面内容才会被替换。
- URL请求过程
接下来,便进入了页面资源请求过程。这时,浏览器进程会通过进程间通信(IPC)把 URL 请求发送至网络进程,网络进程接收到 URL 请求后,会在这里发起真正的 URL 请求流程。
1. 首先,网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有查找到资源,那么直接进入网络请求流程(DNS查询和缓存)。如果请求协议是 HTTPS,那么还需要建立 TLS 连接(HTTPS如何建立连接)。
2. 接下来就是利用 IP 地址和服务器建立 TCP连接。(如何建立连接,关于网络协议那些事)
3. 连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的 Cookie 等数据附加到请求头中,然后向服务器发送构建的请求信息。
4. 服务器接收到请求信息后,会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。
5. 网络进程接收了响应行和响应头之后,就开始解析响应头的内容了。
解析完响应头后,如果发现返回的状态码是 301、302、307、308等,就说明服务器需要浏览器重定向到其他 URL。这时网络进程会从响应头的 Location 字段里面读取重定向的地址,然后再发起新的 HTTP 或者 HTTPS 请求,再从第一步重新开始流程。当然如果响应200,浏览器就可以继续处理该请求
除去状态码,浏览器还会根据服务器返回的Content-Type来针对不同类型的文件做出不同的处理。 例如常见的content-type有下面几个:
- text/html —— 服务器返回的数据是 HTML 格式。
- application/json —— 用于接口的json数据格式。
- application/octet-stream —— 字节流类型,通常用于下载场景
- 准备渲染进程
渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。
- 提交文档
提交文档就是浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:
1. 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
2. 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
3. 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
4. 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,在性能不好的电脑上,之前的页面没有立马消失,而是要加载一会儿才会更新页面。 到这里,一个完整的导航流程就走完了,这之后就要进入渲染阶段了。
- 渲染阶段
一旦文档被提交,渲染进程便开始页面解析和子资源加载了,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后,会停止标签图标上的加载动画。开始进行页面绘制
JavaScript引擎和浏览器内核关系
浏览器内核
-
不同的浏览器有不同的内核组成
-
Gecko:早期被Netscape和Mozilla Firefox浏览器使用
-
Trident:微软开发,被IE4~IE11浏览器使用,但是Edge浏览器已经转向Blink
-
Webkit:苹果基于KHTML开发,开源的,用于Safari,Google Chrome之前在使用
-
Blink:是Webkit的一个分支,Google开发,目前应用于 Google Chrome,Edge,Opera等
-
等等
-
-
事实上,浏览器内核指的是浏览器的排版引擎
- 排版引擎(layout engine),也被称为浏览器引擎(borwer engine),页面渲染引擎(rendering engine)或样板引擎
认识JavaScript引擎
-
为什么需要JavaScript引擎呢
- 上面说过,高级的编辑语言都是需要转成最终的机器指令来执行的
- 事实上我们编写的JavaScript无论你交给浏览器或者Node执行,最后都是需要被CPU执行的
- 但是CPU只认识自己的指令集,实际上是机器语言,才能被CPU所执行
- 所以需要JavaScript引擎帮助我们将JavaScript代码翻译成CPU指令来执行
-
比较常见的JavaScript引擎有哪些呢
- SpiderMonky:第一款JavaScript引擎,由Brendan Eich开发(也就是JavaScript作者)
- Chakra:微软开发,用于IT浏览器
- JavaScriptCore:Webkit中的JavaScript引擎,Apple公司开发
- v8:Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出
- 等等
说完内核和引擎有哪些,下面来讲这两者的关系
-
这里我们以Webkit为例,Webkit 事实上由两部分组成
- Webcore:负责HTML解析、布局、渲染等等相关工作
- JavaScriptCore(jsCore):解析、执行JavaScript代码
-
写过小程序的朋友有没有非常熟悉
- 在小程序中编写的JavaScript代码就是被JSCore所执行的
-
另外一个强大的JavaScript引擎就是V8引擎
V8引擎执行JavaScript代码的过程
-
官方对V8引擎的定义
- V8是用C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等
- 它实现ECMAScript和WebAssembly,并在Window 7或更高版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux系统上运行
- V8可以独立运行,也可以嵌入到任何C++应用程序中
在这个过程中,V8同时使用了Parser(解析器) 、Ignition(解释器) 和TurboFan(优化编译器) 来执行Js代码。
当v8执行javascript源码时候,解析器会把源码解析成抽象语法树(AST),然后解释器再将AST翻译成字节码,一边解释,一边执行。在此过程中,解释器会记特定的代码片段的运行次数,如果运行次数超过了某个阈值,该段代码会被标记为热代码。并将运行信息反馈给优化编译器。优化编译器根据反馈信息,优化并编译字节码。最终生成优化后的机器码,当该段代码继续执行,解释器就直接使用优化机器代码执行,不用再次解释,从而大大提高了代码运行效率,这种在运行时编译代码的技术,也被称为JIT(即时编译)。通过JIT可以极大提升JavaScript代码的执行性能。
- Parser生成抽象语法树
在Chrome中开始下载Javascript文件后,Parser就会开始并行在单独的线程上解析代码。这意味着解析可以在下载完成后仅几毫秒内完成,并生成AST。
AST是把代码结构化成树状结构表示,这样做是为了更好的让编译器或者解释器理解。此外,AST还广泛应用于各类项目中,比如Babel、ESLint,那么AST的生成过程是怎么样的呢?
词法分析(lexical analysis):主要是将字符流(char stream) 转换成标记流(token stream) ,字符流就是我们一行一行的代码,token是指语法上不能再分的、最小的单个字符或者字符串。
var name = "whj"
//转成token后为
[
{
"type": "Keyword",
"value": "var"
},
{
"type": "Identifier",
"value": "name"
},
{
"type": "Punctuator",
"value": "="
},
{
"type": "String",
"value": ""whj""
},
{
"type": "Punctuator",
"value": ";"
}
]
从上面的代码可以看出,var name = "whj"; 这样一段代码,会有关键字"var"、标识符"name"、赋值运算符"="、字符串"whj"、分隔符";",共5个token。
语法分析:将前面生成的token流根据语法规则,形成一个有元素层级嵌套的语法规则树,这个树就是AST。在此过程中,如果源代码不符合语法规则,则会终止,并抛出“语法错误”。
-
Ignition生成字节码
字节码是机器码的抽象,可以看作是小型的构建块,这些构建块组合到一起构成任何JavaScript功能。字节码比机器码占用更小的内存,这也是为什么V8使用字节码的一个很重要的原因。字节码不能够直接在处理器上运行,需要通过解释器将其转换为机器码后才能执行。
Ignition把前一步得到的AST通过字节码生成器经过一些列的优化生成字节码。
在这个过程中:
Register Optimizer: 主要是避免寄存器不必要的加载和存储
Peephole Optimizer: 寻找直接码中可以复用的部分,并进行合并
Dead-code Elimination: 删除无用的代码,减少字节码的大小
通过上面三个过程的优化进一步减小字节码的大小并提高性能,最后Ignition执行优化后的字节码。
-
执行代码及优化
Ignition执行上一步生成的字节码,并记录代码运行的次数等信息,如果同一段代码执行了很多次,就会被标记为 “HotSpot”(热点代码) ,然后把这段代码发送给 编译器TurboFan,然后TurboFan把它编译为更高效的机器码储存起来,等到下次再执行到这段代码时,就会用现在的机器码替换原来的字节码进行执行,这样大大提升了代码的执行效率。
另外,当TurboFan判断一段代码不再为热点代码的时候,会执行去优化的过程,把优化的机器码丢掉,然后执行过程回到Ignition。
V8 简单总结
-
V8引擎本身的源码是非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对 JavaScript 执行的
-
Parse模块会将JavaScript代码转换成AST(抽象语法树),这里因为解释器并不直接认识JavaScript代码
- 如果函数没有被调用,那么是不会被转换成AST的
- Parse的官方V8文档: https://v8.dev/blog/scanner
-
Ignition 是一个解释器,会将AST转换成ByteCode(字节码)
- 同时会手机TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
- 如果函数只调用一次,Ignition会执行解释执行ByteCode
- Ignition的V8官方文档: https://v8.dev/blog/lgnition-interpreter
-
TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码
-
如果一个函数多次被调用,那么就会标记为热点数据,那么就会经过TurboFan转换成优化的机器码,提高代码效率
-
但是,机器码世纪上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化,之前优化的机器码并不能正确处理运算,就会逆向的转换成字节码
-
TurboFan的官方V8官方文档: https://v8.dev/blog/turbofan-jit
-
-
V8 执行细节
-
Blink将源码交给V8引擎,stream获取到源码并且进行编码转换
-
Scanner会进行词法分析(lexical analysis),词法分析会将代码转换成tokens
-
接下来tokens会被转成AST树,经过Parser和PreParser
-
Parser就是直接将tokens转成树结构
-
PreParser称之为预解析,为什么会预解析呢?
-
这里因为并不是所有的JavaScript代码,在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率
-
所以V8引擎就实现了Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是直解析暂时需要的内容,而对函数的全量解析实在函数被调用时才会进行
-
比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析
-
-
-
全局代码执行和作用域提升
示例代码
var name = 'whj'
console.log(name) // whj
var num1 = 20
vat num2 = 30
var result = num1 + num2
console.log(result) // 50
以上代码在运行时到底发生了什么过程
-
在运行之前,代码会被解析,在代码被解析时,V8引擎内部会帮助我们创建一个全局对象(GlobalObject -> go)
- 该对象**所有的作用域(scope)**都可以访问
- 里面会包含
Date
、Array
、String
、Number
等等 - 其中还有一个
window
指向自己
// 伪代码
var GlobalObject = {
String: '类',
Date: '类',
window: GlobalObject,
name: undefined,
num1: undefined,
num2: undefined,
result: undefined
...
}
-
运行代码
- V8为了执行代码,V8引擎内部会有一个执行上下文栈(Execution Context Stack)(函数调用栈)
- 因为我们执行的是全局代码,为了全局代码能够正常的执行,需要创建全局上下文(Global Execution Context)(全局代码需要被执行时才会创建)
- Global Execution Context 会被放到
Execution Context Stack
中执行