网页依赖于浏览器这个宿主环境,那么它是如何渲染,怎样显示在屏幕上的呢?
浏览器主要由用户界面、浏览器内核、数据存储、网络等组成。而浏览器内核分为两部分:渲染引擎(Rendering Engine)和 JS 引擎(JavaScript Engine)。
渲染引擎常被称为“浏览器内核”,主要功能是解析 HTML、CSS 进行页面渲染,渲染引擎决定了浏览器如何显示网页的内容以及页面的格式信息。渲染引擎也被称为排版引擎(Layout Engine)、浏览器引擎(Browser Engine)。而 JS 引擎负责 JavaScript 脚本的解析、编译和执行。
早期内核的概念并没有明确区分渲染引擎和 JS 引擎,现在 JS 引擎已经独立出来,而我们常说的浏览器内核通常指的是渲染引擎。我们知道 JavaScript 是 ECMAScript 标准的一种实现,JavaScript 在浏览器端实现还必须包括 DOM 和 BOM。
下面列举一些常见的浏览器引擎(link:https://www.wikiwand.com/en/Browser_engine):
Timeline
苹果要求其平台下浏览器必须使用 WebKit 渲染引擎,所以一些浏览器在不同平台,其内核是不一样的。(查看当前浏览器内核:浏览器内核版本检测(link:https://ie.icoa.cn/))。
一些常见的 JS 引擎(link:https://www.wikiwand.com/en/Chakra_(JavaScript_engine)):
浏览器厂商们有时会给实验性的或者非标准的 CSS 属性和 JavaScript API 添加前缀,这样开发者就可以用这些新的特性进行试验。
CSS 前缀:
- div {
- -webkit-transition: all 4s ease;
- -moz-transition: all 4s ease;
- -ms-transition: all 4s ease;
- -o-transition: all 4s ease;
- transition: all 4s ease;
- }
-
API 前缀:
接口前缀,需要大写的前缀修饰接口名:
属性和方法前缀,需要使用小写的前缀修饰属性或者方法:
- const requestAnimationFrame = window.requestAnimationFrame ||
- window.mozRequestAnimationFrame ||
- window.webkitRequestAnimationFrame ||
- window.oRequestAnimationFrame ||
- window.msRequestAnimationFrame
-
首先,渲染引擎从网络层请求到 HTML 文档,然后进行如下所示的基本流程:
Rendering Engine Basic Flow
以 WebKit 为例,主流程如下:
WebKit Main Flow
至于 Mozilla 的 Gecko,整体流程跟 WebKit 基本相同,术语稍有不同。例如 WebKit 的 Layout(布局),在 Gecko 上称为 Reflow(重排)。由于本文并非两者的差异,因此不展开赘述,详情看 Introduction to Layout in Mozilla Overview(link:https://developer.mozilla.org/zh-CN/docs/Mozilla/Introduction_to_Layout_in_Mozilla)。
大致过程:
需要着重指出的是,这是一个渐进的过程。为了达到更好的用户体验,渲染引擎会力求尽快将内容显示在屏幕上。它不必等到整个 HTML 文档解析完毕之后,就会开始构建 Render Tree 和设置 Layout。在不断接收和处理来自网络的其余内容的同时,渲染引擎会将部分内容解析并显示出来。
其中 DOM 是 Document Object Model 的简写,而 CSSOM 则是 CSS Object Model 的简写。
需要注意的是:
Render Tree 和 DOM Tree 是相对应的,但并不是一一对应的。非可视化的元素不会插入 Render Tree 中,如 HTML 的 head 元素。如果元素的 display 为 none,那么也不会显示在 Render Tree 中,但是 visibility 为 hidden 的元素仍会显示。
解析是渲染引擎中非常重要的一个环节。
解析文档是指将文档转化为有意义的结构,也就是可让代码理解和使用的结构。解析得到的结果通常是代表了文档结构的节点树,它被称作“解析树”或“语法树”。
From source document to parse tree
解析分为两个过程:词法分析(Lexical Analysis)和语法分析(Syntax Analysis)。
词法分析是将输入内容分割成大量有效的标记(token)的过程。token 是语言词汇,是组成内容最小的元素。语法分析是指应用语言的语法规则的过程。
解析器是这样工作的:词法分析器(Lexer)负责将输入内容分割成一个个有效的 token;而解析器(Parser)负责根据语言的语法规则分析文档结构,从而构建出解析树(Parse Tree)。词法分析器知道如何将无关的字符(如空格、换行符等)分离出来。
举个例子,我们在 AST Explorer(link:https://astexplorer.net/#/gist/e2d0fc90d6891eee271cacba32c51ee2/ce16e705e3113d6df7298a4d8a9b7336fb99f1e7) 网站将以下这段 HTML 文档解析成“树”结构。
- <!DOCTYPE html>
- <html>
- <body>
- <h1>Hello,</h1>
- <p id="name">I'm Frankie.</p>
- </body>
- </html>
-
假设我们要访问 <p> 标签的 id 属性值,如果不将其先解析为“树”的话,应该会想到使用正则表达式去匹配源文档,若需要获取的属性各种各样,显然正则表达式会很麻烦。但现在,我们可以通过类似 document.html.body.p.attrs.id 的形式来获取其值。
如果你对浏览器如何将 HTML 生成“抽象语法树”(AST),可以看下这个库:parse5(link:https://github.com/inikulin/parse5)。
很多时候,语法树(解析树)并不是最终的产品。解析通常在翻译过程中使用的,而翻译是指将输入文档转换成另一种格式。编译就是这样一个例子。编译器可以将源代码(source code)编译成机器代码(machine code)。
Compilation flow
网络的模型是同步的。网页作者希望解析器遇到 <script> 标记时立即解析并执行脚本。文档的解析将停止,直到脚本执行完毕。如果脚本是外部的,那么解析过程会停止,直到从网络同步抓取资源完成后再继续。此模型已经使用了多年,也在 HTML4 和 HTML5 规范中进行了指定。作者也可以将脚本标注为 defer,这样它就不会停止文档解析,而是等到解析结束才执行。HTML5 增加了一个选项,可将脚本标记为异步(async),以便由其他线程解析和执行。
WebKit 和 Gecko 都进行了这项优化。在执行脚本时,其他线程会解析文档的其余部分,找出并加载需要通过网络加载的其他资源。通过这种方式,资源可以子在并行连接上加载,从而提高总体速度。
需要注意的是,预解析器不会修改 DOM Tree,而是将这项工作交由主解析器处理。预解析器只会解析外部资源(例如外部脚本、样式表和图片)的引用。