hi - blog

浏览器原理-v8引擎-js执行原理

浏览器原理-v8引擎-js执行原理

浏览器原理-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
  • 并且在TypeScript的官方文档有这么一句话:源于JavScript,归于JavaScript
    • TypeScript只是在JavaScript的一个超集,在它的基础之上进行了扩展
    • 并且最终TypeScript还是需要转换成JavaScript

JavaScript是一门编程语言

  • 为什么这里要强调JavaScript是一门编程语言呢?
    • 事实上我们可以使用更加准确的描述:JavaScript是一门高级编程语言
  • 那么高级编程语言,就有低级编程语言,从编程语言发展历史来说,可以划分为三个阶段
    • 机器语言: 1000000101010101010,一些机器指令
    • 汇编语言: mov、ax、bx,一些汇编命令
    • 高级语言: C、C++、Java、JavaScript、Python
  • 但是计算及它本身是不认识这些高级语言的,所以我们的代码最终还是需要被转换成机器指令

浏览器从下载到渲染整个流程

/Users/weihuijie/Desktop/WechatIMG28732.png

  1. 处理用户输入 当用户在地址栏中输入一个查询关键字时,地址栏会判断输入的关键字的类型:
  • 搜索内容。地址栏会使用浏览器默认的搜索引擎进行页面跳转并搜索
  • 请求的 URL 。比如输入的是 baidu.com,那么地址栏会根据规则,把这段内容加上协议,合成为完整的 URL,如 https://baidu.com。

当判断为请求URL的适合,当前页面就要被替换成新的页面,不过替换之前,浏览器还会给当前页面执行 beforeunload 事件(如果有的话)。当前页面没有监听 beforeunload 事件或者同意了继续后续流程,那么浏览器便进入loading状态

当浏览器开始加载一个地址之后,标签页上的图标便进入了加载状态。但此时页面显示的依然是之前打开的页面内容,并没立即替换为淘宝的页面。因为需要等待提交文档阶段,页面内容才会被替换。

  1. 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 —— 字节流类型,通常用于下载场景
  1. 准备渲染进程

渲染进程准备好之后,还不能立即进入文档解析状态,因为此时的文档数据还在网络进程中,并没有提交给渲染进程,所以下一步就进入了提交文档阶段。

  1. 提交文档

提交文档就是浏览器进程将网络进程接收到的 HTML 数据提交给渲染进程,具体流程是这样的:

1. 首先当浏览器进程接收到网络进程的响应头数据之后,便向渲染进程发起“提交文档”的消息;
2. 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”;
3. 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程;
4. 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。

这也就解释了为什么在浏览器的地址栏里面输入了一个地址后,在性能不好的电脑上,之前的页面没有立马消失,而是要加载一会儿才会更新页面。 到这里,一个完整的导航流程就走完了,这之后就要进入渲染阶段了。

  1. 渲染阶段

一旦文档被提交,渲染进程便开始页面解析和子资源加载了,渲染进程会发送一个消息给浏览器进程,浏览器接收到消息后,会停止标签图标上的加载动画。开始进行页面绘制

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等
    • 它实现ECMAScriptWebAssembly,并在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代码的执行性能。

  1. 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。在此过程中,如果源代码不符合语法规则,则会终止,并抛出“语法错误”。

  1. Ignition生成字节码

    字节码是机器码的抽象,可以看作是小型的构建块,这些构建块组合到一起构成任何JavaScript功能。字节码比机器码占用更小的内存,这也是为什么V8使用字节码的一个很重要的原因。字节码不能够直接在处理器上运行,需要通过解释器将其转换为机器码后才能执行。

    Ignition把前一步得到的AST通过字节码生成器经过一些列的优化生成字节码。

    在这个过程中:

    Register Optimizer: 主要是避免寄存器不必要的加载和存储

    Peephole Optimizer: 寻找直接码中可以复用的部分,并进行合并

    Dead-code Elimination: 删除无用的代码,减少字节码的大小

通过上面三个过程的优化进一步减小字节码的大小并提高性能,最后Ignition执行优化后的字节码。

  1. 执行代码及优化

    Ignition执行上一步生成的字节码,并记录代码运行的次数等信息,如果同一段代码执行了很多次,就会被标记为 “HotSpot”(热点代码) ,然后把这段代码发送给 编译器TurboFan,然后TurboFan把它编译为更高效的机器码储存起来,等到下次再执行到这段代码时,就会用现在的机器码替换原来的字节码进行执行,这样大大提升了代码的执行效率。

    另外,当TurboFan判断一段代码不再为热点代码的时候,会执行去优化的过程,把优化的机器码丢掉,然后执行过程回到Ignition。

V8 简单总结

  • V8引擎本身的源码是非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对 JavaScript 执行的

  • Parse模块会将JavaScript代码转换成AST(抽象语法树),这里因为解释器并不直接认识JavaScript代码

  • 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

以上代码在运行时到底发生了什么过程

  1. 在运行之前,代码会被解析,在代码被解析时,V8引擎内部会帮助我们创建一个全局对象(GlobalObject -> go)

    • 该对象**所有的作用域(scope)**都可以访问
    • 里面会包含DateArrayStringNumber等等
    • 其中还有一个window指向自己
// 伪代码
var GlobalObject = {
    String: '类',
    Date: '类',
    window: GlobalObject,
    name: undefined,
    num1: undefined,
    num2: undefined,
    result: undefined
    ...
}
  1. 运行代码

    • V8为了执行代码,V8引擎内部会有一个执行上下文栈(Execution Context Stack)(函数调用栈)
    • 因为我们执行的是全局代码,为了全局代码能够正常的执行,需要创建全局上下文(Global Execution Context)(全局代码需要被执行时才会创建)
    • Global Execution Context 会被放到Execution Context Stack中执行
Current profile photo
© 2022 hi - blog
京ICP备2022015573号-1