Skip to content

Latest commit

 

History

History
705 lines (480 loc) · 49.4 KB

File metadata and controls

705 lines (480 loc) · 49.4 KB

浏览器工作原理

  • 功能

  • 架构

  • 网络

  • 渲染

    1. HTML 解析
    2. DOM 树构建 + CSS 解析
    3. 渲染树构建
    4. 布局
    5. 绘制
    6. 重排与重绘
  • CSS:可视化模型

  • JavaScript:单线程 + 事件驱动

参考文献

功能特性

浏览器的主要功能就是向服务器发出请求,在浏览器窗口中展示您选择的网络资源。这里所说的资源一般是指 HTML 文档,也可以是 PDF、图片或其他的类型。资源的位置由用户使用 URI(统一资源标示符)指定。

浏览器解释并显示 HTML 文件的方式是在 HTML 和 CSS 规范中指定的。这些规范由网络标准化组织 W3C(万维网联盟)进行维护。

  • 用来输入 URI 的地址栏
  • 前进和后退按钮
  • 书签设置选项
  • 用于刷新和停止加载当前文档的刷新和停止按钮
  • 用于返回主页的主页按钮

技术架构

browser-layers.png

  • 用户界面 - 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示的您请求的页面外,其他显示的各个部分都属于用户界面。
  • 浏览器引擎 - 在用户界面和渲染引擎之间传送指令。
  • 渲染引擎 - 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
  • 网络 - 用于网络调用,比如 HTTP 请求。其接口与平台无关,并为所有平台提供底层实现。
  • 用户界面后端 - 用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台无关的通用接口,而在底层使用操作系统的用户界面方法。
  • JavaScript 解释器 - 用于解析和执行 JavaScript 代码。
  • 数据存储 = 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“网络数据库”,这是一个完整(但是轻便)的浏览器内数据库。

网络知识

浏览器输入 URL 后发生了什么?

timestamp-diagram

  1. You enter a URL into a web browser
  2. The browser looks up the IP address for the domain name via DNS
  3. The browser sends a HTTP request to the server
  4. The server sends back a HTTP response
  5. The browser begins rendering the HTML
  6. The browser sends requests for additional objects embedded in HTML (images, css, JavaScript) and repeats steps 3-5.
  7. Once the page is loaded, the browser sends further async requests as needed.

参考文献

DNS 查询

dns

  1. 浏览器检查域名是否在缓存当中(要查看 Chrome 当中的缓存, 打开 chrome://net-internals/#dns)。
  2. 如果缓存中没有,就去调用 gethostbyname 库函数(操作系统不同函数也不同)进行查询。
  3. gethostbyname 函数在试图进行DNS解析之前首先检查域名是否在本地 Hosts 里,Hosts 的位置(不同的操作系统有所不同)。
  4. 如果 gethostbyname 没有这个域名的缓存记录,也没有在 hosts 里找到,它将会向 DNS 服务器发送一条 DNS 查询请求(使用 53 端口向 DNS 服务器发送 UDP 请求包,如果响应包太大,会使用 TCP 协议)。DNS 服务器是由网络通信栈提供的,通常是本地路由器或者 ISP 的缓存 DNS 服务器。
  5. 如果本地/ISP DNS 服务器没有找到结果,它会发送一个递归查询请求,一层一层向高层 DNS 服务器做查询,直到查询到起始授权机构,如果找到会把结果返回。

参考文献

TCP 连接和关闭

http-connect

HTTP 请求和响应

HTTP 缓存

启发式缓存(Heuristic Expiration)

Memory Cache VS Disk Cache

网络变迁

| Generation | Icon | Technology | Maximum Download Speed | Typical Download Speed | | --- | --- | --- | --- | --- | --- | | 2G | G | GPRS | 0.1Mbit/s | <0.1Mbit/s | | | E | EDGE | 0.3Mbit/s | 0.1Mbit/s | | 3G | 3G | 3G (Basic) | 0.3Mbit/s | 0.1Mbit/s | | | H | HSPA | 7.2Mbit/s | 1.5Mbit/s | | | H+ | HSPA+ | 21Mbit/s | 4Mbit/s | | | H+ | DC-HSPA+ | 42Mbit/s | 8Mbit/s | | 4G | 4G | LTE Category 4 | 150Mbit/s | 12-15Mbit/s | | 4G+ | 4G+ | LTE-Advanced Cat6 | 300Mbit/s | 24-30Mbit/s | | | 4G+ | LTE-Advanced Cat9 | 450Mbit/s | 60Mbit/s | | | 4G+ | LTE-Advanced Cat12 | 600Mbit/s | TBC | | | 4G+ | LTE-Advanced Cat16 | 979Mbit/s | TBC | | 5G | 5G | 5G | 1,000-10,000Mbit/s(1-10Gbit/s) | TBC

Generation Typical Latency
2G 500ms (0.5 seconds)
3G 100ms (0.1 seconds)
4G 50ms (0.05 seconds)
5G 1ms (0.001 seconds)*

渲染引擎

渲染引擎,又称渲染引擎,也被称为浏览器内核,在线程方面又称为 UI 线程。

webkit-architecture.jpg

有哪些渲染引擎?

各大浏览器厂商依照 W3C 标准自行研发的,常见的浏览器内核可以分这四种:Trident、Gecko、Blink、Webkit。

内核 | 浏览器 | 出生年份 | JS 引擎 | 开源 -------| -----| ---------| -------------| --------|---- Trident | IE4 - IE11| 1997 | JScript,9+chakra | Gecko | Firefox | 2004 | SpiderMonkey | MPL | WebKit | Safari,Chromium,Chrome(-2013) ,Android浏览器,ChromeOS,WebOS 等 | 2005| WebCore + JavascriptCore | BSD Blink | Chrome, Opera | 2013 | V8 | GPL Edge | Edge | 2015 | EdgeHTML + Chakra | MIT(chakra)

疑问:浏览器内核,浏览器引擎,渲染引擎,JavaScript 引擎之间的区别和关系?

浏览器内核又可以分为渲染引擎和 JavaScript 引擎,最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。

渲染引擎的工作原理

render-flow.png

summary-of-browser-main-flows.jpg

Webkit VS Gecko

webkitflow.png

geckoflow.jpg

虽然 WebKit 和 Gecko 使用的术语略有不同,但整体流程是基本相同的。

Gecko 将视觉格式化元素组成的树称为“框架树”。每个元素都是一个框架。WebKit 使用的术语是“渲染树”,它由“渲染对象”组成。对于元素的放置,WebKit 使用的术语是“布局”,而 Gecko 称之为“重排”。对于连接 DOM 节点和可视化信息从而创建渲染树的过程,WebKit 使用的术语是“附加”。有一个细微的非语义差别,就是 Gecko 在 HTML 与 DOM 树之间还有一个称为“内容槽”的层,用于生成 DOM 元素。

1. 解析

渲染引擎将开始解析 HTML 文档,并将各标记逐个转化成“内容树”上的 DOM 节点。同时也会解析外部 CSS 文件以及样式元素中的样式数据。

解析是什么?

解析文档是指将文档转化成为有意义的结构,也就是可让代码理解和使用的结构。解析得到的结果通常是代表了文档结构的节点树,它称作解析树或者语法树。解析的过程可以分成两个子过程:词法分析和语法分析。解析器通常将解析工作分给以下两个组件来处理:词法分析器(有时也称为标记生成器),负责将输入内容分解成一个个有效标记;而解析器负责根据语言的语法规则分析文档的结构,从而构建解析树。

- 词法分析是将输入内容分割成大量标记的过程。标记是语言中的词汇,即构成内容的单位。在人类语言中,它相当于语言字典中的单词。
- 语法分析是应用语言的语法规则的过程。

翻译:很多时候,解析树还不是最终产品。解析通常是在翻译过程中使用的,而翻译是指将输入文档转换成另一种格式。编译就是这样一个例子。编译器可将源代码编译成机器代码,具体过程是首先将源代码解析成解析树,然后将解析树翻译成机器代码文档。

自动生成解析器:有一些工具可以帮助您生成解析器,它们称为解析器生成器。您只要向其提供您所用语言的语法(词汇和语法规则),它就会生成相应的解析器。创建解析器需要对解析有深刻理解,而人工创建并优化解析器并不是一件容易的事情,所以解析器生成器是非常实用的。WebKit 使用了两种非常有名的解析器生成器:用于创建词法分析器的 Flex 以及用于创建解析器的 Bison(您也可能遇到 Lex 和 Yacc 这样的别名)。Flex 的输入是包含标记的正则表达式定义的文件。Bison 的输入是采用 BNF 格式的语言语法规则。

HTML 解析
  1. HTML 解析器的任务是将 HTML 标记解析成解析树,HTML 的词汇和语法在 W3C 组织创建的规范中进行了定义。

  2. 所有的常规解析器都不适用于 HTML(我并不是开玩笑,它们可以用于解析 CSS 和 JavaScript)。HTML 并不能很容易地用解析器所需的与上下文无关的语法来定义。

    • 语言的宽容本质。
    • 浏览器历来对一些常见的无效 HTML 用法采取包容态度。
    • 解析过程需要不断地反复。源内容在解析过程中通常不会改变,但是在 HTML 中,脚本标记如果包含 document.write,就会添加额外的标记,这样解析过程实际上就更改了输入内容。
  3. 由于不能使用常规的解析技术,浏览器就创建了自定义的解析器来解析 HTML。HTML5 规范详细地描述了解析算法,此算法由两个阶段组成:标记化和树构建。

html-parser.png

<html>
  <body>
    <p>
      Hello World
    </p>
    <div> <img src="example.png"/></div>
  </body>
</html>

dom.png

ps:DOM 也是由 W3C 组织指定的。请参见 www.w3.org/DOM/DOMTR。

CSS 解析

和 HTML 不同,CSS 是上下文无关的语法,可以使用简介中描述的各种解析器进行解析。事实上,CSS 规范定义了 CSS 的词法和语法

WebKit 使用 Flex 和 Bison 解析器生成器,通过 CSS 语法文件自动创建解析器。

css-parser

解析器都会将 CSS 文件解析成 StyleSheet 对象,且每个对象都包含 CSS 规则。CSS 规则对象则包含选择器和声明对象,以及其他与 CSS 语法对应的对象。

document.styleSheets

css-stylesheet.jpg

理论上来说,应用样式表不会更改 DOM 树,因此似乎没有必要等待样式表并停止文档解析。但这涉及到一个问题,就是脚本在文档解析阶段会请求样式信息。如果当时还没有加载和解析样式,脚本就会获得错误的回复,这样显然会产生很多问题。这看上去是一个非典型案例,但事实上非常普遍。Firefox 在样式表加载和解析的过程中,会禁止所有脚本。

JavaScript 解析

脚本

HTML 文档在解析器遇到 <script> 标记时立即解析并执行脚本,且文档的解析将停止,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。此模型已经使用了多年,也在 HTML4 和 HTML5 规范中进行了指定。

预解析

。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以在并行连接上加载,从而提高总体速度。请注意,预解析器不会修改 DOM 树,而是将这项工作交由主解析器处理;预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。

异步

此外,也可以将脚本标注为“defer”,这样它就不会停止文档解析,而是等到解析结束才执行。HTML5 增加了一个选项,可将脚本标记为异步,以便由其他线程解析和执行。

script-defer-and-async.png

参考文献

2. 构建渲染树

渲染树是由可视化元素按照其显示顺序而组成的树,也是文档的可视化表示。它的作用是让您按照正确的顺序绘制内容。

WebKit 将呈现树中的元素称为渲染器或渲染对象。渲染器知道如何布局并将自身及其子元素绘制出来。 WebKits RenderObject 类是所有呈现器的基类,其定义如下:

class RenderObject{
  virtual void layout();
  virtual void paint(PaintInfo);
  virtual void rect repaintRect();
  Node* node;  //the DOM node
  RenderStyle* style;  // the computed style
  RenderLayer* containgLayer; //the containing z-index layer
}

每一个渲染器都代表了一个矩形的区域,通常对应于相关节点的 CSS 框,这一点在 CSS2 规范中有所描述。它包含诸如宽度、高度和位置等几何信息。 它包含诸如宽度、高度和位置等几何信息。框的类型会受到与节点相关的“display”样式属性的影响。

呈现树和 DOM 树的关系

呈现器是和 DOM 元素相对应的,但并非一一对应。非可视化的 DOM 元素不会插入呈现树中,例如“head”元素。如果元素的 display 属性值为“none”,那么也不会显示在呈现树中(但是 visibility 属性值为“hidden”的元素仍会显示)。

有一些 DOM 元素对应多个可视化对象。它们往往是具有复杂结构的元素,无法用单一的矩形来描述。例如,“select”元素有 3 个呈现器:一个用于显示区域,一个用于下拉列表框,还有一个用于按钮。如果由于宽度不够,文本无法在一行中显示而分为多行,那么新的行也会作为新的呈现器而添加。

有一些呈现对象对应于 DOM 节点,但在树中所在的位置与 DOM 节点不同。浮动定位和绝对定位的元素就是这样,它们处于正常的流程之外,放置在树中的其他地方,并映射到真正的框架,而放在原位的是占位框架。

dom-and-render-object.png

3. 布局

呈现器在创建完成并添加到呈现树时,并不包含位置和大小信息。计算这些值的过程称为布局或重排。布局为每个节点分配一个应出现在屏幕上的确切坐标。

布局是一个递归的过程。它从根呈现器(对应于 HTML 文档的 <html> 元素)开始,然后递归遍历部分或所有的框架层次结构,为每一个需要计算的呈现器计算几何信息。

根呈现器的位置左边是 0,0,其尺寸为视口(也就是浏览器窗口的可见区域)。

所有的呈现器都有一个“layout”或者“reflow”方法,每一个呈现器都会调用其需要进行布局的子代的 layout 方法。

css-box-model.jpg

4. 绘制

渲染引擎会遍历渲染树,由用户界面后端层将每个节点绘制出来。

5. 动态变化(重排和重绘)

在发生变化时,浏览器会尽可能做出最小的响应。因此,元素的颜色改变后,只会对该元素进行重绘。元素的位置改变后,只会对该元素及其子元素(可能还有同级元素)进行布局和重绘。添加 DOM 节点后,会对该节点进行布局和重绘。一些重大变化(例如增大“html”元素的字体)会导致缓存无效,使得整个呈现树都会进行重新布局和绘制。

6. 总结

  1. 渲染引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建渲染树和设置布局。在不断接收和处理来自网络的其余内容的同时,渲染引擎会将部分内容解析并显示出来。。

测试示例

Firefox Quantum

JavaScript 引擎

JavaScript 特性

  • 单线程
  • 非阻塞 I/O
  • 事件驱动

参考文献

为什么要单线程?

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准? —— JavaScript 运行机制详解:再谈Event Loop

参考文献

可以使用多线程操作 UI 吗?

如果 UI 组件(无论是浏览器的 DOM 操作还是移动端的 View 操作)是非线程安全的(什么是线程安全?),假定有两个线程,一个线程在界面修改内容,同时另一个线程删除这个内容,那么以哪个线程结果为准呢?如果实现类似 Java 线程安全的容器,让 UI 组件变得也是线程安全的,那么内部必须存在线程锁机制,但是怎么界定那些 UI 属性能够同时生效呢?而且这样会耗费大量资源并拖慢运行速度。

Thread-Safe Class Design 一文提到:

It’s a conscious design decision from Apple’s side to not have UIKit be thread-safe. Making it thread-safe wouldn’t buy you much in terms of performance; it would in fact make many things slower. And the fact that UIKit is tied to the main thread makes it very easy to write concurrent programs and use UIKit. All you have to do is make sure that calls into UIKit are always made on the main thread. —— 大意为把 UIKit 设计成线程安全并不会带来太多的便利,也不会提升太多的性能表现,甚至会因为加锁解锁而耗费大量的时间。事实上并发编程也没有因为 UIKit 是线程不安全而变得困难,我们所需要做的只是要确保 UI 操作在主线程进行就可以了。

其他客户端也是这样的吗?

  • Android

    android-developer-fundamentals-training-overview.jpg

    1. Android 应用启动时,系统会为应用创建一个名为“主线程”的执行线程,它是应用与 Android UI 工具包组件进行交互的线程,因此,主线程有时也称为 UI 线程。

    2. 系统不会为每个组件实例创建单独的线程,运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。

    3. 除了 UI 主线程外,Android 支持工作线程,用来处理一些复杂的交互计算。但是 Android UI 组件是非线程安全的(什么样才是线程安全呢?类似 Java 线程安全的数据结构 ConcurrentHashMap,参考 如何证明一个数据结构是线程安全的?),不能通过非 UI 线程操作 UI。

      • 不要阻塞 UI 线程 —— 使用工作线程来处理复杂的计算
      • 不要在 UI 线程之外操作 Android UI 组件

    参考

  • iOS

JavaScript 有工作线程吗?

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。

现代浏览器都开始支持 Web Worker,它是独立于 JavaScript 主线程外的后台线程,可以执行任务而不干扰用户界面(也做不到)。

实际应用

单线程怎么解决 I/O 阻塞问题?

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

而 I/O 操作一般都是比较耗时的(比如 Ajax 操作从网络读取数据),假设 I/O 操作会阻塞 JavaScript 线程(很多前端开发人员理所当然的认为 I/O 就是非阻塞的,然而很多编程语言并不是这样,例如 Java 语言的 I/O 操作就是阻塞线程的),这样便导致 Web 页面卡主。

为了避免 I/O 阻塞主线程,JavaScript 语言的设计者将 I/O 操作都交给非主线程处理,调用 I/O 操作的时候只要给个回调函数,其他工作线程处理完 I/O 操作后在将处理结果传给回调函数,而主线程不需要等待 I/O 操作就可以继续处理下个一个任务。

ps:

JavaScript 所有 I/O 都是非阻塞的吗?

答案:否。XMLHttp​Request​ 也支持同步调用,window.alert 也是同步的。—— 参考示例 javascript-io-block.html

Edit javascript-io-block

JavaScript 主线程工作原理

JavaScript 主线程工作原理

ps: 从左到右,从上倒下分析各个部分。

有哪些线程

  • UI 线程
  • JavaScript 线程
  • 浏览器事件触发线程
  • 定时触发器线程
  • 异步 HTTP 请求线程
  • Code Parser Thead:代码编译线程
  • Statistic Collector Thead:统计收集线程
  • Optimistic Thread:优化线程
  • Garbage Collector Thread:垃圾回收线程
  • Rasterizer Thread:光栅线程

总结:

在浏览器主线程中,JavaScript 代码在调用栈 call stack 执行时,可能会调用浏览器的 API,对 DOM 进行操作。也可能执行一些异步任务:这些异步任务如果是以回调的方式处理,那么往往会被添加到 Event queue 当中;如果是以 promise 处理,就会先放到 Job queue 当中。这些异步任务和渲染任务将会在下一个时序当中由调用栈处理执行。具体参考测试示例 browser-event-log 的浏览器事件日志。

思考:

如果调用栈 call stack 运行一个很耗时的脚本,比如解析一个图片,那么 call stack 会被这个复杂任务堵塞。主线程其他任务都要排队,进而阻塞 UI 响应。这时候用户点击、输入、页面动画等都没有了响应。我们一般有两种方案突破上文提到的瓶颈:

  1. 将耗时高、成本高、易阻塞的长任务切片,分成子任务,并异步执行

  2. 另外一个创新性的做法:使用 HTML5 Web worker

参考文献

思考

为什么 Web 应用没有原生应用流畅?

微信小程序的工作原理

TODO