今天来聊一聊程序员尤其是前端,离不开的工具「浏览器」。浏览器只要是学习过计算机的小伙伴都不陌生吧?它的主要功能就是向服务器发出请求,在浏览器窗口中展示HTML文档、PDF、图片、视频等网络内容。这些网络资源的位置由用户使用 URI(统一资源标示符)来指定。
或许在大多数人眼中,浏览器是这样的:
一个展示前端,一个未知的中间层连接着网络世界,甚至!网络世界也可以省略:一台显示器,一个神秘的幕后黑盒。如果你是一个前端开发者。每天的工作都离不开它。想想那每一个令人“不是那么期待”的早晨,每一个争分夺秒完成任务的黄昏,只有浏览器和编辑器一直是你忠实的伙伴。就连你一直离不开的VS Code编辑器,甚至也与浏览器有着莫大的渊源。
本文就来一探究竟,走进这个我们与网络连接最紧密的中间地带。
Chrome V8 系列文章:葡萄的前端杂货铺:详解 Chrome 「V8 」引擎,让你更懂JavaScript !
1、浏览器发展简史
1.1、浏览器的诞生与发展
1990年第一款浏览器WorldWideWeb诞生。但是现代浏览器的雏形却孕育于 1980s年代。一位名叫蒂姆·伯纳斯-李的英国科学家在 1980 年代初期创建了一个名为 Enquire 的计算机程序,当时他在总部位于瑞士的欧洲核研究组织(CERN,以其法文字母表示)工作。该计划旨在使在 CERN 工作的许多不同个人更容易共享信息。
截止2020年浏览器的发展简史:
早期比较有名、有意义的浏览器主要包括:Erwise、ViolaWWW、Mosaic、Netscape Navigator
990年浏览器诞生之后的故事,想必您已经早有耳闻:
- NCSA Mosaic,或简称 Mosaic,是互联网历史上第一个获普遍使用和能够显示图片的网页浏览器。它由伊利诺伊大学厄巴纳-香槟分校的NCSA组织在1993年发表,并于1997年1月7日正式终止开发和支持,这款浏览器在当时大受欢迎。Mosaic的出现,算是点燃了后期互联网热潮的火种之一。后来 Netscape Navigator 浏览器的开发,聘用了许多原有的 Mosaic 浏览器工程师,但是没有采用 Mosaic 网页浏览器的任何代码。而传承网景浏览器代码的后裔为Firefox浏览器。
- Marc Andreesen 与同事 Jim Clark 于 1994 年成立了一家公司,当时 Mosaic 还是最流行的浏览器,它们计划打造出一个比 Mosaic 更好的浏览器,占领市场,让他们变得富有,并改变历史。他们的第一个浏览器被称为 Mosaic Netscape 0.9,不久更名 Netscape。得益于 JavaScript(JavaScript诞生于1995年,它是Netscape的Brendan Eich 仅花费十天设计实现的。) 和“partial-screen loading”(即使页面未完全加载,用户也可以开始阅读页面上的详细信息,这一个新概念极大地丰富了在线体验)等功能,它很快成为市场领导者,占据了浏览器市场上一半的份额,最疯狂的时候,网景浏览器的市场份额接近百分之九十。
- Netscape 最初的成功向那些在计算机和互联网领域工作的人证明时代已经永远改变了,这让当时业内最强大的参与者感到震惊,一家名为 Microsoft 的西雅图公司就是其中之一。计算机将通过浏览器运行,浏览器可以在任何机器上运行,从而使软件行业民主化并降低其相当大的进入壁垒,这导致许多人猜测操作系统的时代已经结束。Netscape 对微软来说是一个挑战,微软在 1990 年代后期创建了自己的浏览器 Internet Explorer,当时的IE和现在一样,通常被视为劣质产品。由于微软已经建立了销售其专有操作系统 Windows 的帝国,因此将这种由 Netscape 等公司带头的发展视为一种威胁。微软通过对其产品的大量投资,使其与 Netscape 一样好,成功地迅速扭转了浏览器行业的局面。Windows 计算机在发布时已经安装了 Internet Explorer(Microsoft 的浏览器),这使其能够在市场上占据一席之地并不断发展壮大,最终在浏览器领域取得了胜利,这便是著名的第一次浏览器大战。
市场份额的快速下滑导致 Netscape 被出售给了 AOL,2003年7月,网景解散,就在解散的当天,Mozilla基金会成立,2004年基于Mozilla源码的Firefox首次登台,拉开了第二次浏览器大战的序幕。2008 年Netscape最终灭绝,当年的浏览器帝国正式退出了历史的舞台。
到 2003 年,微软的 Internet Explorer 控制了 92% 以上的市场,完全扭转了 1995 年的局面。然而,虽然微软在不到十年的时间里成功地完全接管了浏览器市场,但很快就会出现其他竞争,再次重塑网络浏览器的历史。
- 微软在 1990 年代后期崛起并让 Netscape 等公司屈服之后,浏览器的历史似乎已经走到了尽头。然而,正如最初发布后的情况一样,Internet Explorer 正在成为劣质产品。谷歌于 2008 年推出了其专有浏览器——Chrome。到 2012 年底,即推出仅四年后,谷歌 Chrome 浏览器凭借其易用性、跨平台功能、速度以及与标签和书签相关的特殊功能,取代 Internet Explorer 成为最受欢迎的浏览器。
- 在 2000 年代初期,可能是在微软将浏览器附加到其操作系统之后,Apple 发布了 Safari,一种专为 Mac 设计的浏览器,并成为目前市场上第二大浏览器。
- Internet Explorer 的流行度在 2000 年代后期逐渐减少,主要是因为它变得缓慢和过时,而 Microsoft 发现自己现在似乎已经是在外面观察浏览器世界。该公司不想继续错过,于是着手解决这个问题,但发现一个关键问题是“Internet Explorer”这个名字已经成为劣质浏览器的同义词。因此,为了尝试重新进入游戏,微软不得不重新命名,是以,Edge 便诞生了。Edge是微软浏览器的最新版本,它受到了很多好评,但对于 Microsoft 来说,Edge 的出现可能为时已晚。
- IE浏览器终成时代之泪,Microsoft Edge 成为Windows 11的默认浏览器。这是Windows系统更新20年来,IE的首次缺席,也是最后一次。早在Win10更新时微软就表示,将放弃更新IE转向开发新的浏览器Microsoft Edge。如今是彻底要和桌面上的IE说再见了。 —— IE 浏览器将从 Windows 11 中消失,它也将在 2022 年安息。
1.2、浏览器市场份额
浏览器使用趋势变化:
浏览器市场份额:
国内浏览器市场份额:
2、浏览器架构
2.1、计算机的核心
三层计算机体系结构:底部是机器硬件、中间是操作系统、顶部是应用程序。
当你在电脑或手机上启动应用时,是CPU 和 GPU 为应用供能。通常情况下应用是通过操作系统提供的机制在 CPU 和 GPU 上运行。
2.2、CPU
中央处理器(Central Processing Unit),或简称为 CPU。CPU 可以看作是计算机的大脑。一个 CPU 核心如图中的办公人员,可以逐一解决很多不同任务。它可以在解决从数学到艺术一切任务的同时还知道如何响应客户要求。过去 CPU 大多是单芯片的。随着现代硬件发展,你经常会有不止一个内核,为你的手机和笔记本电脑提供更多的计算能力。
2.3、GPU
图形处理器(Graphics Processing Unit,简称为 GPU)是计算机的另一部件。与 CPU 不同,GPU 擅长同时处理跨内核的简单任务。顾名思义,它最初是为解决图形而开发的。这就是为什么在图形环境中“使用 GPU” 或 “GPU 支持”都与快速渲染和顺滑交互有关。近年来随着 GPU 加速计算的普及,仅靠 GPU 一己之力也使得越来越多的计算成为可能。
2.4、进程与线程
进程:可以被描述为是一个应用的执行程序。线程:是位于进程内部并执行其进程程序的任意部分。
启动应用时会创建一个进程。程序也许会创建一个或多个线程来帮助它工作。操作系统为进程提供了一个可以使用的“一块”内存,所有应用程序状态都保存在该私有内存空间中。关闭应用程序时,相应的进程也会消失,操作系统会释放内存(下图中,边界框为进程,线程作为抽象鱼在进程中游动)。
进程可以请求操作系统启动另一个进程来执行不同的任务。此时,内存中的不同部分会分给新进程。如果两个进程需要对话,他们可以通过进程间通信(IPC)来进行。许多应用都是这样设计的,所以如果一个工作进程失去响应,该进程就可以在不停止应用程序不同部分的其他进程运行的情况下重新启动。
3、浏览器的进程/线程架构模型
3.1、浏览器进程分类
关于如何构建 web 浏览器并不存在标准规范,一个浏览器的构建方法可能与另一个迥然不同。不同浏览器的进程/线程架构一般由下图几部分:
3.2、Chrome多进程架构
而当下“浏览器世界的王者” Chrome 架构如下图所示,渲染进程下显示了多个层,表明 Chrome 为每个标签页运行多个渲染进程。
上图中,顶部是浏览器进程,它与处理应用其它模块任务的进程进行协调。对于渲染进程来说,创建了多个渲染进程并分配给了每个标签页。Chrome 在可能的情况下会给每个标签页分配一个进程。而现在它试图给每个站点分配一个进程,包括 iframe。
- 浏览器进程:控制应用中的 “Chrome” 部分,包括地址栏,书签,回退与前进按钮,以及处理 web 浏览器中网络请求、文件访问等不可见的特权部分;
- 渲染进程:控制标签页内网站展示;
- 插件进程:控制站点使用的任意插件,如 Flash;
- GPU进程:处理独立于其它进程的 GPU 任务。GPU 被分成不同进程,因为 GPU 处理来自多个不同应用的请求并绘制在相同表面。
可以简单理解为不同进程对应浏览器 UI 的不同部分:
Chrome 更多的是把自己抽象为一个操作系统,网页或扩展相当于一个个程序,你甚至可以发现,Chrome 确实自带了一个任务管理器,在任务管理器面板会列出当前正在运行的进程以及它们当前的 CPU/内存使用量情况等信息。一般你可以通过两种方法打开Chrome任务管理器:
- 通过在浏览器顶栏(标签tab栏)右侧右键,选择任务管理器查看;
- 点击 Chrome 浏览器右上角的“选项”菜单(一般是三个点的标识),选择“更多工具”子菜单,点击“任务管理器”,打开任务管理器窗口。
前文中提到了 Chrome 使用多个渲染进程,那他有什么优势呢?
稳定性:最简单的情况下,你可以想象每个标签页都有自己的渲染进程。假设你打开了三个标签页,每个标签页都拥有自己独立的渲染进程。如果某个标签页失去响应,你可以关掉这个标签页,此时其它标签页依然运行着,可以正常使用。如果所有标签页都运行在同一进程上,那么当某个失去响应,所有标签页都会失去响应,显然这样的体验会很糟糕。
安全性与沙箱化:把浏览器工作分成多个进程的另一好处是安全性与沙箱化。由于操作系统提供了限制进程权限的方法,浏览器就可以用沙箱保护某些特定功能的进程。例如,Chrome 浏览器可以限制处理用户输入(如渲染器)的进程的文件访问的权限。
由于进程有自己的私有内存空间,所以它们通常包含公共基础设施的拷贝(如Chrome V8引擎)。这意味着使用了更多的内存,如果它们是同一进程中的线程,就无法共享这些拷贝(同一个进程中的线程不共享堆栈,堆栈是保证线程独立运行所必须的)。为了节省内存,Chrome 对可启动的进程数量有所限制。具体限制数值依设备可提供的内存与 CPU 能力而定,但是当 Chrome 运行时达到限制时,会开始在同一站点的不同标签页上运行同一进程。
Chrome 正在经历架构变革,它转变为将浏览器程序的每一模块作为一个服务来运行,从而可以轻松实现进程的拆解或聚合。具体表现是,当 Chrome 运行在强力硬件上时,它会将每个服务分解到不同进程中,从而提升稳定性,但是如果 Chrome 运行在资源有限的设备上时,它会将服务聚合到一个进程中从而节省了内存占用。在这一架构变革实现前,类似的整合进程以减少内存使用的方法已经在 Android 类平台上使用。
Chrome 67 版本后,桌面版 Chrome 都默认开启了站点隔离,每个标签页的 iframe 都有一个单独的渲染进程。启用站点隔离是多年来工程人员努力的结果。站点隔离并不只是分配不同的渲染进程这么简单。它从根本上改变了 iframe 的通信方式。在一个页面上打开开发者工具,让 iframe 在不同的进程上运行,这意味着开发者工具必须在幕后工作,以使它看起来无缝。即使运行一个简单的 Ctrl + F 来查找页面中的一个单词,也意味着在不同的渲染器进程中进行搜索。你可以看到为什么浏览器工程师把发布站点隔离功能作为一个重要里程碑!
4、浏览器整体架构
如果您是一名前端工程师,那么,面试时你大概率会被问到过:从 URL 输入到页面展现到底发生了什么?,如果您对这一过程不太熟悉,建议看看下面两篇文章,在此不过多赘述:
浏览器的主要任务之一就是渲染展示页面,不同的浏览器内核,渲染过程也不完全相同,但大致流程都差不多,下面这张图片是火狐浏览器(Firefox,可以认为是Netscapede的涅槃重生)开发文档中的一张图片。
上面这张图片大体揭示了浏览器的渲染展示流程,但是从浏览器的整体架构上来说,上面的图片展示的也许只是浏览器体系中的冰山一角。通常意义下,浏览器架构是如下图这样的:
1、用户界面
包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
2、浏览器引擎
用户界面和渲染引擎的桥梁,在用户界面和渲染引擎之间传送指令。浏览器引擎提供了开始加载URL资源 和一些其他高级操作方法,比如:重新加载、前进、后退动作,错误信息、加载进度等。
3、渲染引擎
负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
所谓浏览器内核就是指浏览器最重要或者说核心的部分"Rendering Engine",译为"渲染引擎"。负责对网页语法的解析,比如HTML、JavaScript,并渲染到网页上。所以浏览器内核也就是浏览器所采用的渲染引擎,渲染引擎决定这浏览器如何显示页面的内容和页面的格式信息。不同的浏览器内核对语法的解释也不相同,因此同一网页在不同内核的浏览器显示的效果也会有差异(浏览器兼容)。这也就是网页开发者在不需要同内核的浏览器中测试网页显示效果的原因。
葡萄的前端杂货铺:详解 Chrome V8 引擎让你更懂JavaScript,快来看看它是如何工作的?「三万字!值得收藏」
延伸阅读:曾红极一时的红芯浏览器,官网对其介绍是:拥有智能的认证引擎、渲染引擎、管控引擎,而且还有强大的“国密通讯协议”,支持统一管控、远程控制。2018年8月15日,红芯浏览器被爆出打开安装目录后出现大量和谷歌Chrome浏览器一致的同名文件,其安装程序的文件属性中也显示了原始文件名chrome.exe,红芯浏览器的官网已撤下了浏览器的下载链接。8月16日,红芯联合创始人高婧回应,红芯浏览器“包含‘Chrome’在里面”,但并非抄袭,而是“站在巨人的肩膀上去做创新”。
言归正传,浏览器内核主要包括以下三个技术分支:排版渲染引擎、 JavaScript引擎,以及其他。
排版引擎:
- KHTML:KHTML,是HTML网页排版引擎之一,由KDE所开发。KHTML拥有速度快捷的优点,但对错误语法的容忍度则比Mozilla产品所使用的Gecko引擎小。苹果电脑于2002年采纳了KHTML,作为开发Safari浏览器之用,并发布所修改的最新及过去版本源代码。后来发表的开源WebCore及WebKit引擎,它们均是KHTML的衍生产品。
- WebCore:WebCore是苹果公司开发的排版引擎,它是在另外一个排版引擎“KHTML”的基础上而来的。使用WebCore的主要有Safari浏览器。
浏览器的内核引擎,基本上是四分天下:
- Trident: IE 以Trident 作为内核引擎;
- Gecko: Firefox 是基于 Gecko 开发;
- WebKit: 诞生于1998年,并于2005年由Apple公司开源,Safari, Google Chrome,傲游3,猎豹浏览器,百度浏览器 opera浏览器 基于 Webkit 开发。
- Presto: Opera的内核,但由于市场选择问题,主要应用在手机平台--Opera mini。(2013年2月Opera宣布转向WebKit引擎,2013年4月Opera宣布放弃WEBKIT,跟随GOOGLE的新开发的blink引擎。)
需要略作补充的是,我们经常还会听到Chromium、Webkit2、Blink这些引擎。
-
- Chromium:基于webkit,08年开始作为Chrome的引擎,Chromium浏览器是Chrome的实验版,实验新特性。可以简单地理解为:Chromium为实验版,具有众多新特性;Chrome为稳定版。
- Webkit2:2010年随OS X Lion一起面世。WebCore层面实现进程隔离与Google的沙箱设计存在冲突。
- Blink:基于Webkit2分支,是WebKit中WebCore组件的一个分支,13年谷歌开始作为Chrome 28的引擎集成在Chromium浏览器里。Android的WebView同样基于Webkit2,是现在对新特性支持度最好的内核。Opera(15及往后版本)和Yandex浏览器中也在使用。
- 移动端基本上全部是 Webkit 或 Blink 内核(除去 Android 上腾讯家的 X5),这两个内核对新特性的支持度较高,所以新特性可以在移动端大展身手
对浏览器的内核进行溯源的话,你会发现其实并没有那么多。 Google 的 Blink 来源于苹果开源的 Webkit,而 Webkit 来源于 KHTML, 属于 KDE(K桌面环境,K Desktop Environment的缩写) 的一部分, KDE 又是基于挪威公司 Trolltech 开发的 QT, 很多 KHTML 的贡献者其实也是来自于 Trolltech。各内核关系图:
下面我们以WebKit为列,进行简单介绍,以便让你对渲染引擎有一个更多的理解。WebKit由多个重要模块组成,通过下图我们可以对WebKit有个整体的了解:
WebKit就是一个页面渲染以及逻辑处理引擎,前端工程师把HTML、JavaScript、CSS这“三驾马车”作为输入,经过WebKit的处理,就输出成了我们能看到以及操作的Web页面。从上图我们可以看出来,WebKit由图中框住的四个部分组成。而其中最主要的就是WebCore和JSCore(或者是其它JS引擎)。除此之外,WebKit Embedding API是负责浏览器UI与WebKit进行交互的部分,而WebKit Ports则是让Webkit更加方便的移植到各个操作系统、平台上,提供的一些调用Native Library的接口,比如在渲染层面,在iOS系统中,Safari是交给CoreGraphics处理,而在Android系统中,Webkit则是交给Skia。WebKit的渲染流程:
首先浏览器通过URL定位到了一堆由HTML、CSS、JS组成的资源文件,通过加载器把资源文件给WebCore。之后HTML Parser会把HTML解析成DOM树,CSS Parser会把CSS解析成CSSOM树。最后把这两棵树合并,生成最终需要的渲染树,再经过布局,与具体WebKit Ports的渲染接口,把渲染树渲染输出到屏幕上,成为了最终呈现在用户面前的Web页面。
4、网络
用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现,负责网络通信和安全。
5、JavaScript 解释器
用于解析和执行 JavaScript 代码,执行结果将传递给渲染引擎来展示。
6、用户界面后端
用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
7、数据存储
这是持久层,浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整而轻便的浏览器内数据库。
5、求同存异的浏览器架构
下面列出了部分浏览器的架构图,也许有些架构已经改变,有兴趣可以简单参考看看,除了IE之外,大体上各浏览器的整体架构都是类似的。
Mosaic架构:
Firefox架构:
Chrome架构:
Safari架构:
IE架构:
6、浏览器基本原理
6.1、Chrome V8
参考这篇文章吧:
葡萄的前端杂货铺:详解 Chrome V8 引擎让你更懂JavaScript,快来看看它是如何工作的?「三万字!值得收藏」
V8是依托Chrome发展起来的,后面确不局限于浏览器内核。发展至今V8应用于很多场景,例如流行的nodejs,weex,快应用,早期的RN。V8曾经历过一次比较大的架构调整,主要变化在于“从字节码的放弃到真香”。
6.2、V8 的早期架构
V8引擎诞生的使命就是要在速度和内存回收上进行革命。JavaScriptCore的架构是采用生成字节码的方式,然后执行字节码。Google觉得JavaScriptCore这套架构不行,生成字节码会浪费时间,不如直接生成机器码快。所以V8在前期的架构设计上是非常激进的,采用了直接编译成机器码的方式。后期的实践证明Google的这套架构速度是有改善,但是同时也造成了内存消耗问题。
早期的V8有Full-Codegen和Crankshaft两个编译器。V8 首先用 Full-Codegen把所有的代码都编译一次,生成对应的机器码。JS在执行的过程中,V8内置的Profiler筛选出热点函数并且记录参数的反馈类型,然后交给 Crankshaft 来进行优化。所以Full-Codegen本质上是生成的是未优化的机器码,而Crankshaft生成的是优化过的机器码。
随着网页的复杂化,V8也渐渐的暴露出了自己架构上的缺陷:
- Full-Codegen 编译直接生成机器码,导致内存占用大;
- Full-Codegen 编译直接生成机器码,导致编译时间长,导致启动速度慢;
- Crankshaft 无法优化try,catch和finally等关键字划分的代码块;
- Crankshaft 新加语法支持,需要为此编写适配不同的Cpu架构代码。
3、V8 的现有架构
为了解决上述缺点,V8借鉴JavaScriptCore的架构,生成字节码。V8采用生成字节码的方式后,整体流程如下图:
现在的 V8 是一个非常复杂的项目,有超过 100 万行 C++代码。它由许多子模块构成,其中这 4 个模块是最重要的:
- Parser:负责将 JavaScript 源码转换为 Abstract Syntax Tree (AST)
确切的说,在“Parser”将 JavaScript 源码转换为 AST前,还有一个叫”Scanner“的过程,具体流程如下:
- Ignition:interpreter,即解释器,负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;解释器执行时主要有四个模块,内存中的字节码、寄存器、栈、堆。Ignition的原始动机是减少移动设备上的内存消耗。在Ignition之前,V8的Full-codegen基线编译器生成的代码通常占据Chrome整体JavaScript堆的近三分之一。这为Web应用程序的实际数据留下了更少的空间。Ignition的字节码可以直接用TurboFan生成优化的机器代码,而不必像Crankshaft那样从源代码重新编译。Ignition的字节码在V8中提供了更清晰且更不容易出错的基线执行模型,简化了去优化机制,这是V8 自适应优化的关键特性。最后,由于生成字节码比生成Full-codegen的基线编译代码更快,因此激活Ignition通常会改善脚本启动时间,从而改善网页加载。
- TurboFan:compiler,即优化编译器,利用 Ignition 所收集的类型信息,将 Bytecode 转换为优化的汇编代码;TurboFan项目最初于2013年底启动,旨在解决Crankshaft的缺点。Crankshaft只能优化JavaScript语言的子集。例如,它不是设计用于使用结构化异常处理优化JavaScript代码,即由JavaScript的try,catch和finally关键字划分的代码块。很难在Crankshaft中添加对新语言功能的支持,因为这些功能几乎总是需要为九个支持的平台编写特定于体系结构的代码。
- Orinoco:garbage collector,垃圾回收模块,负责将程序不再需要的内存空间回收。
采用新的Ignition+TurboFan架构后,比Full-codegen+Crankshaft架构内存降低一半多,且70%左右的网页速度得到了提升。
在运行 C、C++以及 Java 等程序之前,需要进行编译,不能直接执行源码;但对于 JavaScript 来说,我们可以直接执行源码(比如:node test.js
),它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation),简称为 JIT。因此,V8 也属于 JIT 编译器。
7、JavaScriptCore
8未诞生之前,早期主流的JavaScript引擎是JavaScriptCore引擎。JavaScriptCore(以下简称JSCore)主要服务于Webkit浏览器内核,他们都是由苹果公司开发并开源出来。JSCore是WebKit默认内嵌的JS引擎,之所以说是默认内嵌,是因为很多基于WebKit分支开发的浏览器引擎都开发了自家的JS引擎,其中最出名的就是前文提到的Chrome的V8。这些JS引擎的使命都是解释执行JS脚本。而在渲染流程上,JS和DOM树之间存在着互相关联,这是因为浏览器中的JS脚本最主要的功能就是操作DOM树,并与之交互。我们可以通过下图看下它的工作流程:
JavaScriptCore主要模块:
- Lexer 词法分析器,将脚本源码分解成一系列的Token
- Parser 语法分析器,处理Token并生成相应的语法树
- LLInt 低级解释器,执行Parser生成的二进制代码
- Baseline JIT 基线JIT(just in time 实时编译)
- DFG 低延迟优化的JIT
- FTL 高通量优化的JIT
可以看到,相比静态编译语言生成语法树之后,还需要进行链接,装载生成可执行文件等操作,解释型语言在流程上要简化很多。这张流程图右边画框的部分就是JSCore的组成部分:Lexer(词法分析)、Parser(语法分析)、LLInt以及JIT(解释执行)的部分(之所以JIT的部分是用橙色标注,是因为并不是所有的JSCore中都有JIT部分)。
词法分析很好理解,就是把一段我们写的源代码分解成Token序列的过程,这一过程也叫分词。在JSCore,词法分析是由Lexer来完成(有的编译器或者解释器把分词叫做Scanner,比如Chrome v8)。
跟人类语言一样,我们讲话的时候其实是按照约定俗成,交流习惯按照一定的语法讲出一个又一个词语。那类比到计算机语言,计算机要理解一门计算机语言,也要理解一个语句的语法。Parser会把Lexer分析之后生成的token序列进行语法分析,并生成对应的一棵抽象语法树(AST)。之后,ByteCodeGenerator会根据AST来生成JSCore的字节码,完成整个语法解析步骤。
JS源代码经过了词法分析和语法分析这两个步骤,转成了字节码,其实就是经过任何一门程序语言必经的步骤–编译。但是不同于我们编译运行OC代码,JS编译结束之后,并不会生成存放在内存或者硬盘之中的目标代码或可执行文件。生成的指令字节码,会被立即被JSCore这台虚拟机进行逐行解释执行。运行指令字节码(ByteCode)是JS引擎中很核心的部分,各家JS引擎的优化也主要集中于此。
PS:严格的讲,语言本身并不存在编译型或者是解释型,因为语言只是一些抽象的定义与约束,并不要求具体的实现,执行方式。这里讲JS是一门“解释型语言”只是JS一般是被JS引擎动态解释执行,而并不是语言本身的属性。
如果对JavaScriptCore有更多兴趣,关于JavaScriptCore的更多细节,建议延伸阅读以下几篇博文:
8、浏览器与JavaScript
这一小结,还是以Chrome V8为例,简单阐述浏览器与JavaScript的关系。
在 V8 出现之前,所有的 JavaScript 虚拟机所采用的都是解释执行的方式,这是 JavaScript 执行速度过慢的一个主要原因。而 V8 率先引入了即时编译(JIT)的双轮驱动的设计(混合使用编译器和解释器的技术),这是一种权衡策略,混合编译执行和解释执行这两种手段,给 JavaScript 的执行速度带来了极大的提升。V8 出现之后,各大厂商也都在自己的 JavaScript 虚拟机中引入了 JIT 机制,所以目前市面上 JavaScript 虚拟机都有着类似的架构。另外,V8 也是早于其他虚拟机引入了惰性编译、内联缓存、隐藏类等机制,进一步优化了 JavaScript 代码的编译执行效率。
8.1、V8 执行一段 JavaScript 的流程
V8 执行一段 JavaScript 的流程如下图所示:
结合上文介绍的Chrome V8 架构,聚焦到JavaScript上,浏览器拿到JavaScript源码,Parser,Ignition 以及 TurboFan 可以将 JS 源码编译为汇编代码,其流程图如下:
简单地说,Parser 将 JS 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将 Bytecode 转换为经过优化的 Machine Code(实际上是汇编代码)。
- 如果函数没有被调用,则 V8 不会去编译它。
- 如果函数只被调用 1 次,则 Ignition 将其编译 Bytecode 就直接解释执行了。TurboFan 不会进行优化编译,因为它需要 Ignition 收集函数执行时的类型信息。这就要求函数至少需要执行 1 次,TurboFan 才有可能进行优化编译。
- 如果函数被调用多次,则它有可能会被识别为热点函数,且 Ignition 收集的类型信息证明可以进行优化编译的话,这时 TurboFan 则会将 Bytecode 编译为 Optimized Machine Code(已优化的机器码),以提高代码的执行性能。
图片中的红色虚线是逆向的,也就是说 Optimized Machine Code 会被还原为 Bytecode,这个过程叫做 Deoptimization。这是因为 Ignition 收集的信息可能是错误的,比如 add 函数的参数之前是整数,后来又变成了字符串。生成的 Optimized Machine Code 已经假定 add 函数的参数是整数,那当然是错误的,于是需要进行 Deoptimization。
1 2 3 4 5 6 |
function add(x, y) { return x + y; } add(1, 2); add('1', '2'); |
V8 本质上是一个虚拟机,因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段:
- 第一种是将高级代码转换为二进制代码,再让计算机去执行;
- 另外一种方式是在计算机安装一个解释器,并由解释器来解释执行。
- 解释执行和编译执行都有各自的优缺点,解释执行启动速度快,但是执行时速度慢,而编译执行启动速度慢,但是执行速度快。为了充分地利用解释执行和编译执行的优点,规避其缺点,V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码。
简单总结如下,V8 执行一段 JavaScript 代码所经历的主要流程包括:
- 初始化基础环境;
- 解析源码生成 AST 和作用域;
- 依据 AST 和作用域生成字节码;
- 解释执行字节码;
- 监听热点代码;
- 优化热点代码为二进制的机器代码;
- 反优化生成的二进制机器代码。
9、Chrome V8 的事件机制
关于异步编程和消息队列,UI 线程提供一个消息队列,并将待执行的事件添加到消息队列中,然后 UI 线程会不断循环地从消息队列中取出事件、执行事件,通用 UI 线程宏观架构如下图所示:
10、浏览器的不同形态
10.1、WebView
WebView 是一种嵌入式浏览器,原生应用可以用它来展示网络内容。WebView 只是一个可视化的组件/控件/微件等。这样我们可以用它来作为我们原生 app 的视觉部分。当你使用原生应用时,WebView 可能只是被隐藏在普通的原生 UI 元素中,你甚至用不到注意到它。
如果你把浏览器想象成两部分,一部分是 UI(地址栏,导航栏按钮等),其它部分是把标记跟代码转换成我们可见和可交互视图的引擎。WebView 就是浏览器引擎部分,你可以像插入 iframe 一样将 Webview 插入到你的原生应用中,并且编程化的告诉它将会加载什么网页内容。
运行在你的 WebView 中的 JavaScript 有能力调用原生的系统 API。这意味着你不必受到 Web 代码通常必须遵守的传统浏览器安全沙箱的限制。下图解释了使用这种技术后的架构差异:
默认情况下,在 WebView 或 Web 浏览器中运行的任何 Web 代码都与应用的其余部分保持隔离。这样做是出于安全原因,主要是为降低恶意的 JavaScript 代码对系统造成的伤害。对于任意 Web 内容,这种安全级别很有意义,因为你永远不能完全信任加载的 Web 内容。但 WebView 的情况并非如此,对于 WebView 方案,开发人员通常可以完全控制加载的内容。恶意代码进入并在设备上造成混乱的可能性非常低。
这就是为什么对于 WebView,开发人员可以使用各种受支持的方式来覆盖默认的安全行为,并让 Web 代码和原生应用代码相互通信。这种沟通通常称为 bridge。你可以在上文的图片中看到 bridge 可视化为 Native Bridge 和 JavaScript Bridge 的一部分。
WebView 非常好,虽然它看起来像是完全特殊和独特的,但请记住,它们只不过是一个在应用中设置好位置和大小的、没有任何花哨 UI 的浏览器,这就是它的精髓。大多数情况下,除非您调用原生 API,否则您不必在 WebView 中专门测试您的 Web 应用程序。此外,您在 WebView 中看到的内容与您在浏览器中看到的内容相同,尤其是使用同一渲染引擎时:
- 在 iOS 上,Web 渲染引擎始终是 WebKit,与 Safari 和 Chrome 相同。是的,你没看错。iOS 上的 Chrome 实际上使用了 WebKit。
- 在 Android 上的渲染引擎通常是 Blink,与 Chrome 相同。
- 在 Windows,Linux 和 macOS 上,由于这些是更宽松的桌面平台,因此在选择 WebView 风格和渲染引擎时会有很大的灵活性。你看到的流行渲染引擎将是 Blink(Chrome)和 Trident(Internet Explorer),但是没有一个引擎可以依赖。这完全取决于应用以及它正在使用的 WebView 引擎。
WebView 的应用:
- WebView 最常见的用途之一是显示链接的内容;
- 广告仍然是原生应用最流行的赚钱方式之一,大多数广告是通过 WebView 提供的 Web 内容进行投放的;
- Hybrid Apps,混合应用程序很受欢迎有几个原因,最大的一个是提高开发人员的生产力。如果你有一个可以在浏览器中运行的响应式 Web 应用程序,那么让相同的应用程序在各种设备上与混合应用程序一起运行是相当简单的;当你对 Web 应用进行更新时,所有使用它的设备都可以立即使用该更改,因为内容来自一个集中的服务器,而如果是纯原生应用,部署和更新时,你将不得不经历针对每个平台的构建、审核;
- 原生应用扩展,如 Microsoft Office 中类似维基百科这样的基于网络的扩展就是通过一个 WebView 实现的。
如果你对 WebView 感兴趣,可通过以下几篇文章继续了解:
10.2、Headless browser
无头浏览器是一种未配置图形用户界面 (GUI) 的 Web 浏览器,通常通过命令行或网络通信来执行。它主要由软件测试工程师使用,没有 GUI 的浏览器执行速度更快,因为它们不必绘制视觉内容。无头浏览器的最大好处之一是它们能够在没有 GUI 支持的服务器上运行。
Headless 浏览器对于测试网页特别有用,因为它们能够像浏览器一样呈现和理解超文本标记语言,包括页面布局、颜色、字体选择以及JavaScript和AJAX的执行等样式元素,这些元素在使用其他测试方法时通常是不可用的。
Headless 浏览器有两个主要可交付成果:
- 无头库,它允许嵌入应用程序控制浏览器并与网页交互。
- 一个无头外壳,它是一个示例应用程序,用于执行无头 API 的各种功能。
Puppeteer 是一个 Node 库,他提供了一组用来操纵 Chrome 的 API, 通俗来说就是一个 headless chrome 浏览器 (当然你也可以配置成有 UI 的,默认是没有的)。既然是浏览器,那么我们手工可以在浏览器上做的事情 Puppeteer 都能胜任, 另外,Puppeteer 翻译成中文是”木偶”意思,所以听名字就知道,操纵起来很方便,你可以很方便的操纵她去实现:
1) 生成网页截图或者 PDF
2) 高级爬虫,可以爬取大量异步渲染内容的网页
3) 实现 UI 自动化测试,模拟键盘输入、表单自动提交、点击、登录网页等
4) 捕获站点的时间线,以便追踪你的网站,帮助分析网站性能问题
5) 模拟不同的设备
6) ...
Puppeteer 跟 webdriver 以及 PhantomJS 最大的 的不同就是它是站在用户浏览的角度,而 webdriver 和 PhantomJS 最初设计就是用来做自动化测试的,所以它是站在机器浏览的角度来设计的,所以它们 使用的是不同的设计哲学。
10.3、Electron
Electron(原名为Atom Shell)是 GitHub 开发的一个开源框架。它通过使用 Node.js(作为后端)和Chromium 的渲染引擎(作为前端)完成跨平台的桌面 GUI 应用程序的开发。现已被多个开源 Web 应用程序用于前端与后端的开发,著名项目包括 GitHub 的 Atom 和微软的 Visual Studio Code。
Electron Architecture 由多个 Render Process 和一个 Main 进程组成。 Main Process 启动Render Process,它们之间的通信是通过IPC [Inter Process Communication],如下图所示。
我们常用的IDE VSCode 就是基于 Electron (原来叫 Atom Shell) 进行开发的。如下图所示,(点击 VSCode 帮助【Help】 下的 切换开发人员工具即可打开以下面板)。
VS Code 的其他的主要组件有:
- 壳:Monaco Editor
- 内核:Language Server Protocol(一个代码编辑器)
- Debug Adapter Protocol
- Xterm.js
延伸阅读:
- 浏览器简史
- Web 浏览器相关的一些概念
- 浏览器的工作原理:新式网络浏览器幕后揭秘
- 从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理
- 移动端 JS 引擎哪家强?美国硅谷找......
- 从 V8 角度揭秘你不知道的面试八股文
- 高性能 JavaScript 引擎 V8 - 垃圾回收
- Inside look at modern web browser【一共四篇,可供参考】
浏览器代码兼容性测试:
参考资料
- Inside look at modern web browser
- 浏览器是如何工作的:Chrome V8 让你更懂 JavaScript
- 深入理解JSCore
- The Story of the Web: A History Of Internet Browsers
- PPT - Browser Architecture
- JavaScript 引擎 V8 执行流程概述
- Understanding WebViews
- Quantum Up Close: What is a browser engine?
全文行文结构大概如下: