# 前言

读完本文希望你能了解:

  • 浏览器的架构、线程、进程
  • 浏览器一次导航背后发生了什么
  • 概念的关联:浏览器内核、JS引擎、线程、进程
  • V8是如何工作的

本人原文原址,懒得搬图片了 (opens new window)

# 浏览器架构

硬件、操作系统、软件的分层架构 浏览器是如何划分模块的,并且分配进程或线程给这些模块运行

# CPU、GPU、操作系统、应用的关系

CPU 即中央处理器,可以处理几乎所有计算。以前的 CPU 是单核的,现在大部分笔记电脑都是多核的,专业服务器甚至有高达 100 多核的。CPU 计算能力很强,但只能一件件事处理。 **GPU **一开始是为图像处理设计的,即主要处理像素点,所以拥有大量并行的处理简单事物的能力,非常适合用来做矩阵运算,而矩阵运算又是计算机图形学的基础,所以大量用在可视化领域。 CPU、GPU 都是计算机硬件,这些硬件各自都提供了一些接口供汇编语言调用;而操作系统则基于它们之上用 C 语言(或其它)将硬件管理了起来,包括进程调度、内存分配、用户内核态切换等等;运行在操作系统之上的则是应用程序了,所以应用程序不直接和硬件打交道,而是通过操作系统间接操作硬件。 jiagou.png

为什么应用程序不能直接操作硬件呢? 这样做有巨大的安全隐患,因为硬件是没有任何抽象与安全措施的,这意味着理论上一个网页可以通过 js 程序,在你打开网页时直接访问你的任意内存地址,读取你的聊天记录,甚至读取历史输入的银行卡密码进行转账操作。

# 在进程和线程上执行程序

为了让程序运行的更安全,操作系统创造了进程与线程的概念,进程可以分配独立的内存空间,进程内可以创建多个线程进行工作,这些线程共享内存空间。 因为线程间共享内存空间,因此不需通信就能交流,但内存地址相互隔离的进程间也有通信需求,需通过 IPC(Inter Process Communication)进行通信。 进程之间相互独立,即一个进程挂了不会影响到其它进程,还可以一个进程中可以创建一个新进程,并与之通信,所以浏览器就采用了这种策略,将 UI、网络、渲染、插件、存储等模块进程独立,并且任意挂掉后都可以被重新唤起。 get.svg1.svg

# 浏览器有哪些进程

下表展示每个 Chrome 进程与各自控制的内容:

进程 控制
Browser进程 负责浏览器页面显示,与用户交互,如前进后退等。负责各个页面的管理,创建和销毁其它进程。
Renderer进程 控制标签页内网站展示。默认每个Tab一个进程,内部是多线程的。
Plugin
进程 控制站点使用的任意插件,如 Flash。
GPU
进程 处理独立于其它进程的 GPU 任务。GPU 被分成不同进程,因为 GPU 处理来自多个不同应用的请求并绘制在相同表面。

mianProcess.png 图 9:不同进程指向浏览器 UI 的不同部分 还有更多进程如扩展进程与应用进程。如果你想要了解有多少进程运行在你的 Chrome 浏览器中,可以点击右上角的选项菜单图标,选择更多工具,然后选择任务管理器。然后会打开一个窗口,其中列出了当前正在运行的进程以及它们当前的 CPU/内存使用量。

# 浏览器架构

那么浏览器是怎么使用进程和线程来工作的呢? 其实大概可以分为两种架构,一种是单进程架构,也就是只启动一个进程,这个进程里面有多个线程工作。 第二种是多进程架构,浏览器会启动多个进程,每个进程里面有多个线程,不同进程通过IPC进行通信。 jiagoux.png 以chrome举例: chrome.png Chrome浏览器会有一个浏览器进程(browser process),这个进程会和其他进程一起协作来实现浏览器的功能。对于渲染进程(renderer process),Chrome会尽可能为每一个tab甚至是页面里面的每一个iframe都分配一个单独的进程。

# Chrome 多进程架构的优缺点

优点:

  • 避免单个page crash影响整个浏览器
  • 避免第三方插件crash影响整个浏览器
  • 多进程充分利用多核优势
  • 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性

缺点:占内存。 由于进程有自己的私有内存空间,所以它们通常包含公共基础设施的拷贝(如 V8)。这意味着使用了更多的内存,如果它们不是同一进程中的线程,就无法共享这些拷贝。 为了节省内存,Chrome 对可启动的进程数量有所限制。具体限制数值依设备可提供的内存与 CPU 能力而定,但是当 Chrome 运行时达到限制时,会开始在同一站点的不同标签页上运行同一进程。

# 节省更多内存 —— Chrome 中的服务化

Chrome 正在经历架构变革,它转变为将浏览器程序的每一模块作为一个服务来运行,从而可以轻松实现进程的拆解或聚合。 浏览器有许多独立的模块(进程和线程),比如:

  • 浏览器模块(Browser):负责整个浏览器内行为协调,调用各个模块。
  • 网络模块(Network):负责网络 I/O。
  • 存储模块(Storage):负责本地 I/O。
  • 用户界面模块(UI):负责浏览器提供给用户的界面模块。
  • GPU 模块:负责绘图。
  • 渲染模块(Renderer):负责渲染网页。
  • 设备模块(Device):负责与各种本地设备交互。
  • 插件模块(Plugin):负责处理各类浏览器插件。

通常观点是当 Chrome 运行在强力硬件上时,它会将每个服务分解到不同进程中,从而提升稳定性,但是如果 Chrome 运行在资源有限的设备上时,它会将服务聚合到一个进程中从而节省了内存占用。在这一架构变革实现前,类似的整合进程以减少内存使用的方法已经在 Android 类平台上使用。 jieou.svg

# 特别嘉宾:每个 iframe 的渲染进程 —— 站点隔离

站点隔离 (opens new window) 将同一个 tab 内不同 iframe 包裹在不同的进程内运行,以确保 iframe 间资源的独占性,以及安全性。 同源策略 (opens new window) 是 web 的核心安全模型。同源策略确保站点在未得到其它站点许可的情况下不能获取其数据。安全攻击的一个主要目标就是绕过同源策略。进程隔离是分离站点的最高效的手段。 iframe.webp

# 导航时发生了什么

上个小节我们研究了不同的进程和线程如何处理浏览器的不同部分。 这个小节研究每个进程和线程如何通信以显示网站。

# 第 1 步:处理输入内容

当用户开始在地址栏键入时,UI 线程要问的第一件事是 “这是一次搜索查询还是一个 URL 地址?”。在 Chrome 中,地址栏同时也是一个搜索输入栏,所以 UI 线程需要解析和决定把你的请求发送到搜索引擎,或是你要请求的网站。 2-1.webp

# 第 2 步:开始获取网页内容

当用户按下 Enter 键时,UI 线程启用network 线程获取网页内容。network thread 会寻找合适的协议处理网络请求,一般会通过 DNS 协议 (opens new window) 寻址,通过 TLS 协议 (opens new window) 建立安全链接。如果服务器返回了比如 301 重定向信息,network thread 会通知 UI thread 这个信息,再启动一遍第二步。 2-2.webp

# 第 3 步:读取响应

2-3.webp 一旦开始收到响应主体(payload),network thread 会读取响应内容。在这一步 network thread 会首先读取首部一些字节,即我们常说的响应头,其中包含 Content-Type (opens new window) 告知返回内容是什么。如果返回内容是 HTML,则 network thread 会将数据传送给 renderer process,但是如果是一个压缩文件或是其他文件,那么意味着它是一个下载请求,因此需要将数据传递给下载管理器。 2-4.webp 此时也会进行 SafeBrowsing (opens new window) 检查。如果域名和响应数据似乎匹配到一个已知的恶意网站,那么网络线程会显示一个警告页面。除此之外,还会发生 CrossOriginReadBlocking(CORB) (opens new window)检查,以确保敏感的跨域数据不被传给渲染进程。

# 第 4 步:查找渲染进程

一旦所有检查都完成,network thread 会通知 UI thread 已经准备好跳转了(注意此时并没有加载完所有数据,第三步只是检查了首字节),UI thread 会通知 renderer process 进行渲染。 2-5.webp 由于网络请求会花费几百毫秒才获取回响应,因此可以应用一个优化措施。为了提升性能,UI thread 在通知 network thread 的同时就会实例化一个 renderer process 等着,一旦 network thread 完毕后就可以立即进入渲染阶段,如果检查失败则丢弃提前实例化的 renderer process。

# 第 5 步:提交导航

现在渲染进程已经就绪,browser process 通过 IPC 向 renderer process 传递数据流。当browser process收到渲染进程已经就绪的消息时,导航完毕并且文档加载解析开始。 此时导航会被确认,浏览器的各个状态(比如导航状态、前进后退历史)将会被修改,同时为了方便 tab 关闭后快速恢复,会话记录会被存储在硬盘。 2-7.webp

# 额外的步骤:初始加载完毕

一旦导航被提交,渲染进程开始加载资源和渲染页面。当 renderer process 加载完成后(具体做了什么下一篇会说明),会通知 browser process onLoad 事件,此时浏览器完成最终加载完毕状态,loading 圆圈也会消失,各类 onLoad 的回调触发。 之所以说“结束”,是因为客户端 JavaScript 可以在这时之后仍然加载额外的资源并且渲染新视图(初始加载结束)。 2-8.webp

# 渲染进程处理网站内容

这小节宏观的介绍 renderer process 做了哪些事情。 浏览器 tab 内 html、css、javascript 内容基本上都由 renderer process 的主线程处理,除了一些 js 代码会放在 web worker 或 service worker 内,所以浏览器主线程核心工作就是解析 web 三剑客并生成可交互的用户界面。 3-1.webp

# 1、解析(Parsing)- 构建DOM

# DOM 的构建

当渲染进程收到导航的提交消息并开始接收 HTML 数据时,主线程(实际上是GUI渲染线程)开始解析文本字符串(HTML)并将其转换为文档对象模型(DOM)。将 HTML 到 DOM 的解析由 HTML Standard (opens new window) 规定。

# tip1:子资源加载

网站通常使用图像、CSS 和 JavaScript 等外部资源,这些文件需要从网络或缓存加载。在解析构建 DOM 时,主线程按处理顺序逐个请求它们,但为了加快速度,“预加载扫描器(preload scanner)”会同时运行。如果 HTML 文档中有 之类的内容,则预加载扫描器会查看由 HTML 解析器生成的标记,并在浏览器进程中向网络线程发送请求。 3-2.webp

# tip2:JavaScript 阻塞解析

当 HTML 解析器遇到 <script> 标记时,会暂停解析 HTML 文档,开始加载、解析并执行 JavaScript 代码。 这是为什么呢? 因为JavaScript 可以使用诸如 document.write() 的方法来改写文档,这会改变整个 DOM 结构(HTML 规范里的 overview of the parsing model (opens new window) 中有一张不错的图片)。这就是 HTML 解析器必须等待 JavaScript 运行后再继续解析 HTML 文档原因。

# tip3:提示浏览器如何加载资源

开发者可以通过多种方式向浏览器发送提示,以便很好地加载资源。 如果你的 JavaScript 不使用 document.write(),你可以在 <script> 标签添加 async (opens new window)defer (opens new window) 属性,这样浏览器会异步加载运行 JavaScript 代码,而不阻塞解析。 如果合适,你也可以使用 JavaScript 模块 (opens new window)(ES modules)。 可以使用 告知浏览器当前导航肯定需要该资源,并且你希望尽快下载。

# 2、样式计算

只有 DOM 是不够的,style 标签申明的样式需要作用在 DOM 上,所以基于 DOM,浏览器要生成 CSSOM,这个 CSSOM 主要是基于 css 选择器(selector)确定作用节点的。 3-2.webp 即使你不提供任何 CSS,每个 DOM 节点都具有计算样式。像 h1 标签看起来比 h2 标签大,每个元素都有 margin,这是因为浏览器具有默认样式表。

# 3、布局

现在渲染进程知道了:每个节点的样式和文档的结构,但这不足以渲染页面。想象一下,你正试图通过手机向朋友描述一幅画:“这里有一个大红圈和一个小蓝方块”,这并不能让你的朋友知道这幅画究竟长什么样。 3-3.webp 布局是计算元素几何形状的过程。主线程遍历 DOM,计算样式并创建布局树,其中包含 x y 坐标和边界框大小等信息。布局树可能与 DOM 树结构类似,但它仅包含页面上可见内容相关的信息。 如果一个元素应用了 display:none,那么该元素不是布局树的一部分(但 visibility:hidden 的元素在布局树中)。类似地,如果应用了如 p::before{content:"Hi!"} 的伪类,则即使它不在 DOM 中,也包含于布局树中。 3-4.webp 由于换行而移动的盒子布局 (opens new window) 确定页面布局是一项很有挑战性的任务。即使是从上到下的块流这样最简单的页面布局,也必须考虑字体的大小以及换行位置,这些因素会影响段落的大小和形状,进而影响下一个段落的位置。 CSS 可以使元素浮动到一侧、隐藏溢出的元素、更改书写方向。可以想象到这一阶段的任务之艰巨。Chrome 浏览器有整个工程师团队负责布局。

# 4、绘制

3-5.webp 图 7:一个人拿着笔站在画布前,思考着她应该先画圆形还是先画方形 拥有 DOM、样式和布局仍然不足以渲染页面,因为布局树仅决定了物理结构,但不决定元素的上下空间结构。假设你正在尝试重现一幅画。你知道元素的大小、形状和位置,但你仍需要判断绘制它们的顺序。 例如,可以为某些元素设置 z-index,此时按 HTML 中编写的元素的顺序绘制会导致错误的渲染。 3-6.webp 在绘制步骤中,主线程遍历布局树,创建绘制记录。绘制记录是绘图过程的记录,就像是“背景优先,然后是文本,然后是矩形”。如果你使用过 JavaScript 绘制了canvas元素,那么这个过程对你来说可能很熟悉。 3-7.webp

# 5、合成

# 如何绘制一个页面?

现在浏览器知道文档的结构、每个元素的样式、页面的几何形状和绘制顺序,它是如何绘制页面的呢? 把这些信息转换为屏幕上的像素,我们称为光栅化。 处理这种情况的一种简单的方法是,先在光栅化视窗内的画面,如果用户滚动页面,则移动光栅框,并填充光栅化缺少的部分。这就是 Chrome 首次发布时处理光栅化的方式,这样做会导致渲染永远滞后于滚动。 但是,现代浏览器会运行一个更复杂的过程,我们称为合成。 简单光栅处理示意动画 (opens new window)

# 什么是合成

合成处理示意动画 (opens new window) 合成是一种将页面的各个部分分层,分别光栅化,并在称为合成线程的单独线程中合成页面的技术。 即将渲染内容分层绘制与渲染,如果发生滚动,由于图层已经光栅化,因此它所要做的只是合成一个新帧。动画也可以以相同的方式(移动图层和合成新帧)实现。 你可以在 DevTools 使用 Layers 面板 (opens new window) 看看你的网站如何被分层。

# 分层

为了分清哪些元素位于哪些图层,主线程遍历布局树创建图层树(此部分在 DevTools 性能面板中称为“Update Layer Tree”)。如果页面的某些部分应该是单独图层(如滑入式侧面菜单)但没拆分出来,你可以使用 CSS 中的 will-change 属性来提示浏览器。 3-9.webp 你可能想要为每个元素都分层,但是合成大量的图层可能会比每帧都光栅化页面的刷新方式更慢,因此测量应用程序的渲染性能至关重要。

# 主线程的光栅化和合成

一旦创建了图层树并确定了绘制顺序,主线程就会将该信息提交给合成线程。接着,合成线程会光栅化每个图层。一个图层可能会跟整个页面一样大,因此合成线程将它们分块后发送到光栅线程。光栅线程光栅化每个小块后会将它们存储在显存中。 3-10.webp 合成线程会给不同的光栅线程设置优先级,以便视窗(或附近)内的画面可以先被光栅化。图层还具有多个不同分辨率的块,可以处理放大操作等动作。 一旦块被光栅化,合成线程会收集这些块的信息(称为绘制四边形)创建合成帧

  • 绘制四边形:包含诸如图块在内存中的位置,以及合成时绘制图块在页面中的位置等信息
  • 合成帧:一个绘制四边形的集合,代表一个页面的一帧。

接着,合成帧通过 IPC提交给浏览器进程。此时,可以从 UI 线程或其他插件的渲染进程添加另一个合成帧。这些合成器帧被发送到 GPU 然后在屏幕上显示。如果接收到滚动事件,合成线程会创建另一个合成帧发送到 GPU。 3-11.webp 合成的好处是它可以在不涉及主线程的情况下完成。合成线程不需要等待样式计算或 JavaScript 执行。 这就是为什么仅合成动画 (opens new window)被认为是流畅性能的最佳选择。 如果需要再次计算布局或绘制,则必须涉及主线程。

# 用户输入行为与合成器

前面介绍了浏览器的基础进程、线程以及它们之间协同的关系,并重点说到了渲染进程是如何处理页面绘制的,那么最后一节聊到浏览器是如何处理页面中事件的

# 浏览器视角下的输入事件

当你听到“输入事件”(input events)的时候,你可能只会想到:在文本框中输入内容、或者对页面进行了点击操作。从浏览器的角度来看的话,输入其实代表着来自于用户的任何手势动作(gesture)。所以用户滚动页面,触碰屏幕以及移动鼠标等操作都可以看作来自于用户的输入事件。 当用户做了一些诸如触碰屏幕的手势动作时,浏览器进程(browser process)是第一个可以接收到这个事件的地方。可是浏览器进程只能知道用户的手势动作发生在什么地方而不知道如何处理,这是因为标签内(tab)的内容是由页面的渲染进程(render process)负责的。因此浏览器进程会将事件的类型(如touchstart)以及坐标(coordinates)发送给渲染进程。为了可以正确地处理这个事件,渲染进程会找到事件的目标对象(target)然后运行这个事件绑定的监听函数(listener) 4-1.webp

# 合成器接收输入事件

(opens new window) 图 2:悬于页面图层的视图窗口 在上节里,我们探讨了合成器如何通过合成栅格化图层,实现流畅的页面滚动。如果页面上没有添加任何事件监听,合成器线程会创建独立于主线程的新合成帧。但要是页面上添加了事件监听呢?合成器线程又是如何得知事件是否需要处理的?

# 理解非立即可滚动区

运行 JavaScript 脚本是主线程的工作。所以页面合成后,合成线程会将页面里添加了事件监听的区域标记为“非立即可滚动区”(Non-fast Scrollable Region)。有了这个信息,如果输入事件发生在这一区域,合成线程可以确定应将其发往主线程处理。如输入事件发生在这一区域之外,合成线程则确定无需等待主线程,而继续合成新帧。 4-2.webp

# 设置事件处理器时须注意

web 开发中常用的事件处理模式是事件代理。因为事件会冒泡,所以你可以在最顶层的元素中添加一个事件处理器,用来代理事件目标产生的任务。下面这样的代码,你可能见过,或许也写过。

document.body.addEventListener('touchstart', event => {
  if (event.target === area) {
    event.preventDefault();
  }
}); 

这样只需添加一个事件处理器,即可监听所有元素,的确十分省事。如果站在浏览器的角度去考量,这等于把整个页面都标记成了“非立即可滚动区”,意味着即便你设计的应用本不必理会页面上一些区域的输入行为,合成线程也必须在每次输入事件产生后与主线程通信并等待返回。如此则得不偿失,使原本能保障页面滚动流畅的合成器没了用武之地。 4-4.webp 你可以给事件监听添加一个 passive:true (opens new window) 选项 ,将这种负面效果最小化。这会提示浏览器你想继续在主线程中监听事件,但合成器不必停滞等候,可接着创建新的合成帧。

document.body.addEventListener('touchstart', event => {
  if (event.target === area) {
    event.preventDefault();
  }
}, {passive: true}); 

# 事件合并

由于事件触发频率可能比浏览器帧率还要高(1 秒 120 次),如果浏览器坚持对每个事件都进行响应,而一次事件都必须在 js 里响应一次的话,会导致大量事件阻塞,因为当 FPS 为 60 时,一秒也仅能执行 60 次事件响应,所以事件积压是无法避免的。 为了解决这个问题,浏览器在针对可能导致积压的事件,比如滚动事件时,将多个事件合并到一次 js 中,仅保留最终状态。 如果不希望丢掉事件中间过程,可以使用 getCoalescedEvents 从合并事件中找回每一步事件的状态:

window.addEventListener('pointermove', event => {
  const events = event.getCoalescedEvents();
  for (let event of events) {
    const x = event.pageX;
    const y = event.pageY;
    // draw a line using x and y coordinates.
  }
});

# 浏览器内核和JS引擎的关系

浏览器内核(Rendering Engine),是指浏览器最核心的部分,也称“渲染引擎”。 浏览器内核主要可以分成两部分:排版渲染引擎(layout engineer 或者 Rendering Engine或者渲染引擎)、JS 引擎(如V8)。

注意区分渲染引擎和渲染进程 渲染进程除了渲染引擎,还包括与浏览器进程的通讯线程等等。 渲染进程,负责使用渲染引擎对源文件进行解析,生成网页交还给浏览器进程进行呈现。同浏览器进程还负责管理渲染进程。

# 渲染进程是如何管理进程和使用引擎的

渲染进程主要包含:

# GUI渲染线程

  • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
  • 当界面需要重绘或由于某种操作引发回流时,该线程就会执行。
  • 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

# JS引擎线程

  • 负责处理JavaScript脚本程序。(例如调用V8引擎)。
  • JS引擎线程负责解析JavaScript脚本,运行代码。
  • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(render进程)中无论什么时候都只有一个JS线程在运行JS程序。
  • 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。

# 事件触发线程

  • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解成JS引擎自己都忙不过来,需要浏览器另开线程协助)。
  • 当JS引擎执行代码块如setTimeout时(也可来自浏览器内核的其它线程,如鼠标点击,AJAX异步请求等),会将对应任务添加到事件线程中。
  • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。
  • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)。

# 定时触发器线程

  • 传说中的setTimeout和setInterval所在的线程
  • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的,如果处于阻塞线程状态就会影响计时的准确)
  • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
  • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

# 异步http请求线程

  • 在XMLHttpRequest在连接后是通过浏览器新建一个线程请求
  • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中,再由JavaScript引擎执行

2473796310-5aba11e73b2df_fix732.webp

# 回顾一下:渲染进程和Browser主进程的通信过程

  • Browser主进程收到用户请求,首先需要获取页面内容(如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render渲染进程
  • Render渲染进程的Renderer接口收到消息,简单解释后,交给渲染线程GUI,然后开始渲染
  • GUI渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser主进程获取资源和需要GPU进程来帮助渲染
  • 当然可能会有JS线程操作DOM(这可能会造成回流并重绘)
  • 最后Render渲染进程将结果传递给Browser主进程
  • Browser主进程接收到结果并将结果绘制出来

# JS引擎

为什么需要JS引擎? 高级的编程语言都是需要转成最终的机器指令来执行的,事实上我们编写的JavaScript无论你交给浏览器或者Node执行,最后都是需要被CPU执行的。但是CPU只认识自己的指令集,实际上是机器语言,才能被CPU所执行。所以我们需要JavaScript引擎帮助我们将JavaSCript代码翻译成CPU指令来执行。

比较常见的JavaScript引擎有哪些呢?

  • SpiderMonekey:第一款JavaScript引擎,由BrenDan Eich开发(也就是JavaScript作者)
  • Chakra:微软开发,用于IE浏览器
  • JavaScriptCore:WebKit中的JavaScript引擎,Apple公司开发
  • V8:Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出

# 了解V8

定义:

  • V8是用C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等.
  • 它实现ECMAScript和WebAssembly,并在Windows7或更高版本,MacOs10.12+和使用x64,IA-32,ARM或MISP处理器的Linux系统上运行
  • V8可以独立运行,也可以嵌入到任何C++应用程序中

# V8引擎的架构

  • V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的
  • Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码(如果函数没有被调用,那么是不会被转换成AST的)
  • Ignition是一个解释器,会将AST转换成ByteCode(字节码),同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算),如果函数只调用一次,Ignition会执行解释执行ByteCode;
  • TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能

# V8引擎的解析图(官方)

5-9.png

# V8执行的细节

那么我们的JavaScript源码是如何被解析(Parse过程)的呢?

  1. 渲染进程将源码交给V8引擎,Stream获取到源码并且进行编码转换
  2. Scanner会进行词法分析(lexical analysis),词法分析会将代码转换成tokens
  3. 接下来tokens会被转换成AST树,经过Parser和PreParser Parser就是直接将tokens转成AST树架构 PreParser称之为与解析,为什么需要预解析呢?
    • 这是因为并不是所有的JavaScript代码,在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率
    • 所以V8引擎就实现了Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行
    • 预解析过程中,会验证函数语法是否有效,解析变量声明和函数声明,构建作用域(变量提升)
  4. 生成AST树后,会被Ignition转成字节码(bytecode),之后的过程就是代码的编译执行

# 实例分析一次执行过程

v81.c3b8d567.jpeg

# 参考

https://developer.chrome.com/blog/inside-browser-part1/ (opens new window)

精读现代浏览器 (opens new window)