前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >V8系统解读(一): V8 在 Chrome 中的位置&编译调试V8

V8系统解读(一): V8 在 Chrome 中的位置&编译调试V8

作者头像
用户3806669
发布2021-03-11 14:45:20
2.4K0
发布2021-03-11 14:45:20
举报
文章被收录于专栏:前端三元同学前端三元同学

前言

这是一套关于 Chrome 中的 JS 引擎 V8 的文章系列。谈到 V8 ,估计大部分人会一种畏惧感,首先映入脑海的便是天花乱坠的 C++ 源码,但其实就算你掌握了 C++,也很难读懂源码,读懂里面的源码,并不是语言的问题,而是一个工程复杂度的问题。而且,我认为把 V8 里面的源码全部读懂并不是一个明智的做法,一个是工程量过于庞大,很容易失去耐心,另一方面代码的封装度非常高,导致很多抽象的代码,理解难度大。你想啊,几百个顶尖的工程师开发了十几年的东西,会让你轻易就把源码啃下来吗?

所以,我认为对于这种复杂的引擎来说,首先去看的不是具体的实现代码,而是顶层的设计思想和关键的技术手段。这样,如果对一部分特别感兴趣,也能找到一个切入的点去源码中验证,或者拿到更多的细节。

那还有一个问题,为什么要学习 V8,学习它有什么用?

很遗憾地告诉你,从功利的角度说,对你更好、更快地完成业务,并没什么用。如果说真的有用,可能面试的时候会有一点帮助,但更重要的是,作为一个追求极致的前端来说,这是一项基本功。基本功的重要性不言而喻,如果不知道引擎的实现思路,如何写出高质量高性能的 JS 代码简直是天方夜谭。

基于这样的出发点,我开始了这个系列的分享。主要分为两条主线,一个是 V8 的编译执行流程,这会逼着你去补充计算机底层架构相关的知识,另一个是在性能优化方面 V8 采取了哪些具体的手段,根据这些手段你就知道如何合适的组织 JS 代码去触发这些优化,达到极致的性能。下面是本系列的思维导图:

今天是第一篇,来分享一下 V8 在宏观上所处的位置(针对 Chorme 环境)。

Chrome 的多进程架构

早期的 Chrome 是单进程的,渲染、插件和网络等功能都是通过里面的线程来完成,这样大家都是共享一份内存的数据,不需要进程间通信,确实比较方便,但是一旦其中有一个线程出现问题,那个整个浏览器就会直接崩溃。为了解决这个问题,Chrome 后来采用了多进程架构,也就是现在的架构,如下所示:

简单介绍一下这些进程的功能吧。

浏览器主进程:负责界面显示、用户交互、子进程管理,以及与外界通信的能力。

网络进程: 主要负责页面的网络资源加载。关于这个进程外界比较有争议,因为之前它并不是一个独立的进程,而是隶属于主进程当中的一个子功能。

渲染进程: 负责将 HTML、CSS 和 JS 转换为用户可见的网页,其中有一个主线程,这个主线程的执行权会在两个引擎间来回切换,一个是 Blink 排版引擎,一个是 V8 引擎,完成 JS 执行和文档的排版。

GPU进程: 用来绘制 UI 界面。

插件进程: 负责插件的运行。

其他进程: 如实用程序网络服务、辅助框架等等功能,可以通过任务处理器看到。

V8 所处的位置

从上面可以看出 V8 实际上是在渲染进程当中,也就是渲染进程的一部分,与排版引擎 Blink共享一个主线程(这个主线程也叫UI线程)。

所以 V8 所在的宿主环境也就是渲染进程,会给 V8 注入许多运行时的能力,使得在 JS 中能够顺利地调用。

这些能力并不属于 V8, 但对于 V8 的执行确实至关重要的。我们来梳理一下这些能力:

  • 栈空间和堆空间
  • 全局执行上下文
  • EventLoop 调度系统

栈空间和堆空间

基本数据类型用存储,引用数据类型用存储。

:::tip 提示 闭包变量是个例外,后面专门来介绍实现原理 :::

具体而言,以下数据类型存储在栈中:

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol
  • bigint

而所有的对象数据类型存放在堆中。

值得注意的是,对于赋值操作,原始类型的数据直接完整地赋值变量值,对象数据类型的数据则是复制引用地址。

因此会有下面的情况:

代码语言:javascript
复制
let obj = { a: 1 };
let newObj = obj;
newObj.a = 2;
console.log(obj.a);//变成了2

之所以会这样,是因为 obj 和 newObj 是同一份堆空间的地址,改变newObj,等于改变了共同的堆内存,这时候通过 obj 来获取这块内存的值当然会改变。

当然,你可能会问: 为什么不全部用栈来保存呢?

首先,对于系统栈来说,它的功能除了保存变量之外,还有创建并切换函数执行上下文的功能。举个例子:

代码语言:javascript
复制
function f(a) {
  console.log(a);
}

function func(a) {
  f(a);
}

func(1);

假设用ESP指针来保存当前的执行状态,在系统栈中会产生如下的过程:

  1. 调用func, 将 func 函数的上下文压栈,ESP指向栈顶。
  2. 执行func,又调用f函数,将 f 函数的上下文压栈,ESP 指针上移。
  3. 执行完 f 函数,将ESP 下移,f函数对应的栈顶空间被回收。
  4. 执行完 func,ESP 下移,func对应的空间被回收。

图示如下:

因此你也看到了,如果采用栈来存储相对基本类型更加复杂的对象数据,那么切换上下文的开销将变得巨大!

全局执行上下文

分为两部分:全局 API 和 词法环境。

全局 API 包括 window 对象,默认指向 window 的 this 关键字,还包括所有的 Web API,比如 setTimout, XMLHttpRequest,document 等等。你没有听错,这些能力都不是 V8 的,所以跟 V8 的创建和销毁没有关系,当你往这些 API 对象上挂载属性后,会一直会驻留于堆内存中。

词法环境主要包括使用了var、let、const 声明的变量的内容。这些内容存放在栈内存或堆内存中。

EventLoop 调度能力

回顾一下浏览器中 EventLoop 的调度过程:

  1. 一开始整段 JS 脚本作为第一个宏任务执行
  2. 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
  3. 当前宏任务执行完出队,检查微任务队列,如果有则依次执行,直到微任务队列为空
  4. 执行浏览器 UI 线程的渲染工作
  5. 检查是否有Web worker任务,有则执行
  6. 执行队首新的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

这些任务队列和调度的过程都由 Chrome 所提供,V8 只负责将执行具体的任务。

编译和调试 V8

Mac

方法一:Homebrew

如果安装了 Homebrew, 直接通过:

代码语言:javascript
复制
brew install v8

即可, 期间会自动安装 d8 调试工具。

对于一个 demo 文件:

代码语言:javascript
复制
// index.js
var text = '111';

执行以下命令查看 V8 为这段 js 生成的字节码:

代码语言:javascript
复制
d8 --print-bytecode index.js

输出如下:

也可通过这条命令查看所有的命令集:

代码语言:javascript
复制
d8 --help
方法二: jsvu

推荐使用一个非常好用的脚手架——jsvu, 首先安装:

代码语言:javascript
复制
npm i jsvu -g

然后执行下面的命令,添加~/.jsvu到你的PATH:

代码语言:javascript
复制
export PATH="${HOME}/.jsvu:${PATH}"

现在执行:

代码语言:javascript
复制
jsvu

第一步: 根据提示信息选择你所在的操作系统。

第二步: 选择需要安装的 JS 引擎,选择 v8 或者 v8-debug。(后者的打印信息会更加丰富,比如能打印 AST 信息)

现在你就能使用 v8 或者 v8-debug 来执行调试了。也就是需要将方法一中的d8换成v8或者v8-debug,能够达到和方法一中同样的效果。

Windows

首先安装:

代码语言:javascript
复制
npm i jsvu -g

然后添加 .jsvu 的位置到你的 Path,也就是在环境变量中的 Path 中,加入 .jsvu 目录所在的绝对路径(包括 .jsvu 本身)。

(如果环境变量还不知道在哪改的同学,去看百度吧,链接在这里:https://jingyan.baidu.com/article/8ebacdf02d3c2949f65cd5d0.html)

现在执行:

代码语言:javascript
复制
jsvu

第一步: 根据提示信息选择你所在的操作系统。

第二步: 选择需要安装的 JS 引擎,选择 v8 或者 v8-debug。(后者的打印信息会更加丰富,比如能打印 AST 信息)

现在你就能使用 v8 或者 v8-debug 来执行调试了。也就是需要将方法一中的d8换成v8或者v8-debug,能够达到和方法一中同样的效果。

以 v8 命令为例,对于一个 demo 文件:

代码语言:javascript
复制
// index.js
var text = '111';

执行以下命令查看 V8 为这段 js 生成的字节码:

代码语言:javascript
复制
v8 --print-bytecode index.js

输出如下:

也可通过这条命令查看所有的命令集:

代码语言:javascript
复制
v8 --help

总结

V8 处在 Chrome 渲染进程当中,与排版引擎 Blink共享一个主线程。渲染进程作为宿主环境,给 V8 提供了栈内存和堆内存空间全局执行上下文EventLoop调度能力。接着,我们搭建了 v8 的调试环境,能够输出 v8 的中间产物,让我们对 v8 的工作有更加直观的认识。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端三元同学 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • Chrome 的多进程架构
  • V8 所处的位置
    • 栈空间和堆空间
      • 全局执行上下文
        • EventLoop 调度能力
        • 编译和调试 V8
          • Mac
            • 方法一:Homebrew
            • 方法二: jsvu
          • Windows
          • 总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档