概述
对于一直从事B/S架构(B/S也是一种C/S架构,只不过C/S自己写显示而B/S有相对统一的显示引擎实现而已)WEB领域的开发人员来说,浏览器 Browser再熟悉不过了。不管你是JAVA流还是微软流抑或PHP流,WEB开发新流派。不论你的服务器端采用哪种业务架构,ORACLE数据库还是 MSSQL,站在用户的角度思考,这个才是他们直接能接触到的,前端的用户体验给了用户直观的印象,投其所好,我们也总少不了要跟它打交道,很多问题的根源皆来源于此,了解一下其内部执行原理,也许对我们的开发工作会事半功倍,从一位纯粹的开发人员到一位技术领域的专家,也许就在于这些细节中。
本文讨论仅限HTML/CSS类与浏览器显示相关的知识。
不包括javascript浏览器引擎。
不包括客户端与服务器端HTTP通信。
不包括服务器端处理流程部分。
浏览器种类
我们现在经常使用的主要有5种浏览器,Internet Explorer,Firefox,Safari,Chrome跟Opera,包含了市面上3种主流的浏览器网页排版引 擎,WebKi(chrome,safari,opera),Gecko(Mozilla,Firefox 等使用的排版引擎)和Trident(也称为MSHTML,IE 使用的排版引擎)。大家可以跑一下您所使用的浏览器跑分情况http://html5test.com/。其中Firefox,Chrome ,Safari三款浏览器部分源代码是开源的,一些分析工作正式基于其源码进行的分析。顺便说一句, Chrome等开源浏览器市场中所占的份额越来越高(见http://www.w3schools.com/browsers/browsers_stats.asp)(ie6使用情况http://www.ie6countdown.com/), HTML5前端开发利器的推出,HTML支持标准的逐渐统一,IE6(7,8)等低版本的逐渐淘汰,也许往后针对CSS HACK等兼容性场景的需求会越来越少,专注于实现而不是专注于兼容,提高我们的效率,体现思想核心价值体现,这才是开发人员真正该干的事。站在用户的角 度来思考,只是稍微改变一下操作方式换来的是真正的产品易用性与安全性的提升,也是一件好事,希望往后的产品开发都只针对IE9以上版本。
浏览器的主要职责
浏览器,我们天天在用,可是如果哪天面试一个WEB领域的开发,要你说出他的职责,未必人人都能一下子清楚的表述出来。我觉得浏览器的首要功能 就是把用户想要的各种形式WEB资源确认无误的显示出来,通常我们见到的资源格式主要是HTML,当然还包括PDF,图片,视频等,资源的定位通过URI 来实现(Uniform resource Identifier)。
W3C (World Wide Web Consortium) 组织制定了一套HTML/CSS规范,清楚的描述了浏览器如何解释HTML文件以及如何去显示它,W3C 也是WEB领域标准协议的制定组织。目前我们常见的HTML主要为 HTML4.01跟XHTML1.0,由1999年推出(http://www.w3.org/TR/html401/)。 HTML5 规范现在仍处于发展阶段,虽然已接近尾声且各大主流浏览器都有所支持,但仍不是很完善。目前CSS也主要是CSS2.0((http://www.w3.org/TR/CSS2/), 随着时间的推移,硬件的发展,软件行业的进步,HTML5/CSS3黄金组合一定会流行起来的。同样,CSS版本3进行中。以前的浏览器市场是百花争艳, 百家齐鸣,往往只支持大部分的规范内容同时还在发展各自的拓展,这对于WEB开发人员和最终用户来说带来了严重的兼容性问题。幸好,现如今大多数的浏览器 渐渐的修正了这一错误行为。
各大浏览器之间有很多共同点,其中公共的界面元素有:
- 地址栏
- 前进,后退按钮
- 书签设置
- 刷新,停止按钮
- 主页按钮
从浏览器界面上来看,没有相关的规范竟然能做到如此的一致,确实很奇怪。这正是各大浏览器厂商多年来相互的模仿导致的。HTML5协议并未约束 浏览器要有哪些界面元素,只是列出了一些常用的,比如地址栏,状态栏,工具栏。当然各浏览器都有一些各自的特性,如firefox的下载管理器。更多的介 绍起参见以后的Browser User interface(浏览器界面元素)章节,此处不再累牍。
上面说了我们看到的浏览器大体情况,接下来再谈谈浏览器的主要组件有哪些,相互间的关系是怎样的。
浏览器的基础架构
- 界面元素(或界面控件) – 包括地址栏,前进后退,书签菜单等窗口上除了网页显示区域以外的部分。
- 浏览器引擎 – 查询与操作渲染引擎的接口。
- 渲染引擎 – 负责显示请求的内容。比如请求到HTML, 它会负责解析HTML 与 CSS 并将结果显示到窗口中。
- 网络 – 用于网络请求, 如HTTP请求。它包括平台无关的接口和各平台独立的实现。
- UI后端 – 绘制基础元件,如组合框与窗口。它提供平台无关的接口,内部使用操作系统的相应实现。
- JavaScript解释器。用于解析执行JavaScript代码。
- 数据存储。这是一个持久层。浏览器需要把所有数据存到硬盘上,如cookies。新的HTML规范 (HTML5) 规定了一个完整(虽然轻量级)的浏览器中的数据库:’web database’。
需要注意的是,与其它浏览器不同chrome使用多个渲染引擎实例,每个Tab页一个,即每个Tab都是一个独立进程。
渲染过程
用户请求的HTML文本(text/html)通过浏览器的网络层到达渲染引擎后,渲染工作开始。每次渲染文档通常不会超过8K的数据块,其中基础的渲染流程图如下:
详细流程图
渲染引擎首先解析HTML文档,转换为一棵DOM树,此为第一步。接下来不管是内联式,外联式还是嵌入式引入的CSS样式也会被解析,同时生成 另外一棵用于渲染DOM树的树-渲染树(render tree) ,渲染树包含带有颜色,尺寸等显示属性的矩形,这些矩形的顺序与显示顺序基本一致。然后就是对渲染树的每个节点进行布局处理,确定其在屏幕上的显示位置。 最后就是遍历渲染树并用上一章提到的UI后端层将每一个节点绘制出来。
以上步骤是一个渐进的过程,为了提高用户体验,渲染引擎试图尽可能快的把结果显示给最终用户。它不会等到所有HTML都被解析完才创建并布局渲染树。它会在从网络层获取文档内容的同时把已经接收到的局部内容先展示出来。
DOM树
DOM对于前端JAVASCRIPT开发人员来说也许再熟悉不过了,DOM全称为Document Object Model,即我们所说的文档对象模型。我们可以把它看做是HTML元素对外的接口,有了这些接口javascript开发人员才能够实现复杂的页面功 能。DOM树的根节点是Document对象。DOM也是有规范的,可见 http://www.w3.org/DOM/DOMTR。与XML解析器不同的是,HTML相对来说比较宽松,即使不标准的书写方式也会解析出来,不会抛出解析异常之类的错误。
浏览器利用HTML解析器,分析接收到的HTML文本构件出dom object添加到dom树相应位置上。解析的时候会利用DTD(Document Type Definition)规范描述,DTD中包含了所有允许的元素(element)以及这些元素的属性(attribute),以及元素之间可能的层次关系。市面上我们看到很多类型的DTD,那是为了兼容以前的书写的HTML文档,目前标准推荐的DTD规范定义可见http://www.w3.org/TR/html4/strict.dtd,解析HTML正是基于此规范,上头列举了支持的元素类型,元素的属性以及事件等信息。
所有HTML引用的外部资源默认都是异步加载的,即解析与下载资源同时进行,但由于脚本资源(.js)在解析过程中就有可能运行, 如document.write之类的,所以默认脚本资源加载的时候是同步进行的,除非添加了defer或者asyn之类的属性。CSS由于不会更改 DOM对象的结构,其资源的加载是异步的。这样就又会产生一个问题,譬如<link href=a.css> 然后<script src=a.js>,a.js可能用到了a.css里头定义的class,但是由于a.css加载是异步的,a.js却是同步执行的,可能就会报 错,遇到这种情况,浏览器会进行特殊处理,当加载脚本的时候如果当前还在加载样式资源,则加载脚本的操作会被阻塞知道样式文件加载完毕。
当以上解析过程完毕后,浏览器继续进行标记为deferred模式的脚步解析,然后就是整个解析过程的实际结束。文档状态设置为complete,同时触发load事件。
Render树
浏览器在构造DOM树的同时也在构造着另一棵树-Render Tree,与DOM树相对应暂且叫它Render树吧,我们知道DOM树为javascript提供了一些列的访问接口(DOM API),但这棵树是不对外的。它的主要作用就是把HTML按照一定的布局与样式显示出来,用到了CSS的相关知识。从MVC的角度来说,可以将render树看成是V,dom树看成是M,C则是具体的调度者,比HTMLDocumentParser等。
Render树的每一个节点我们叫它renderer,其由下列类型的基础类构造。
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 } |
DOM树与Render树
可以这么说,没有DOM树就没有Render树,但是它们之间可不是简单的一对一的关系。我们已经知道了render树是用于显示的,那不可见的元素当然 不会在这棵树中出现了,譬如<head>,您还能想到哪些呢?除此之外,diplay等于none的也不会被显示在这棵树里头,但是 visibility等于hidden的元素是会显示在这棵树里头的,可以自己想一下为什么。说了这么多render树,我们还没见一下它的真容呢,它到 底会是个什么模样呢?我们看一下图。
与DOM对象类型很丰富啊,什么head,title,div,而Render树相对来说就比较单一了,毕竟它的职责就是为了以后的显示渲染用 嘛。从上图我们还可以看出,有些DOM元素没有对应的renderer,而有些DOM元素却对应了好几个renderer,对应多个renderer的情 况是普遍存在的,就是为了解决一个renderer描述不清楚如何显示出来的问题,譬如select元素,我们就需要三个renderer,one for the display area, one for the drop down list box and one for the button。
上图中还有一种关系未可看出,即renderer与dom元素的位置也可能是不一样的。说的就是那些添加了float:---或者position:absolute的元素,因为它们脱离了正常的文档流顺序,构造Render树的时候会针对它们实际的位置进行构造。
CSS样式
样式来源有下列三种方式
- CSS rules, either in external style sheets or in style elements.
p {color:blue}
- Inline style attributes like
<p style="color:blue" />
- HTML visual attributes (which are mapped to relevant style rules)
<p bgcolor="blue" />
一个元素最终经过计算可能匹配到了很多条样式规则,他们之间存在一定的优先顺序,从低到高有:
- 浏览器默认样式--见http://www.iecss.com/
- 用户浏览器个性化偏好设置
- HTML开发者定义的一般样式
- HTML开发者定义的!important样式
- 用户浏览器中个性化设置!important样式
样式优先级计算公式
- count 1 if the declaration is from is a 'style' attribute rather than a rule with a selector, 0 otherwise (= a)
- count the number of ID attributes in the selector (= b)
- count the number of other attributes and pseudo-classes in the selector (= c)
- count the number of element names and pseudo-elements in the selector (= d)
具体可见http://www.w3.org/TR/CSS2/cascade.html#specificity
布局与显示
上面确定了renderer的样式规则后,然后就是重要的显示因素布局了。当renderer构造出来并添加到render树上之后,它并没有位置跟大小信息,为它确定这些信息的过程,我们就称之为布局。
从页面开发角度说,拿到一个HTML页面,明确了页面要实现的功能之后,首先开始动手做的就是布局啦,其它譬如背景图片,字体,文字啦之类的样式问题还好 说,布局却是让人有点感觉经常与其打交道却经常的感觉驾驭不了它,至于界面逻辑或者交互设计虽然也比较难但是至少能让人摸着头脑,所以有必要细说一下这个 布局。平时大家浏览网页遇到好看的布局也一定要多花点时间去分析分析它,非前端开发人员多掌握一点界面设计或者交互设计之类的知识未尝不是一件好事,更何 况现在好多情况都是一套软件产品从设计到出品整套制造过程都要参与,很有收缩性。
浏览器进行页面布局基本过程是以浏览器可见区域为画布,左上角为(0,0)基础坐标,从左到右,从上到下从DOM的根节点开始画,首先确定显示元素的大小 跟位置,此过程是通过浏览器计算出来的,用户CSS中定义的量未必就是浏览器实际采用的量,灵活的布局方式却是靠经验以及设计领域相关经验等得来的。如果 显示元素有子元素得先去确定子元素的显示信息,如果页面布局完后有改动能不整体重新布局就不重新布局,当然由于脚本或者用户操作也有可能随时会被重新布 局,那就回到了本段路起点。
高度和宽度
显示元素的高度(height),宽度(width)的确定。元素的高度由子元素遍历计算得来,而宽度由本身确定。
最终所有显示元素被构建成一个盒子类似的东西构成了整个布局效果,盒子模型效果图如下:
Box Model详细描述可见http://www.w3.org/TR/CSS2/box.html
BOX TYPE
每个显示元素上还有一个重要属性就是display及z-index,display不同的类型使用不同的渲染方式,即使没有手动去指定它本身也会存在一个默认值,具体默认值情况请参见 http://www.w3.org/TR/CSS2/sample.html.
block类型的盒子具有自己的一块区域,里头养了很多inline box。各个block box按上下顺序部件,各个inline box按从左到右顺序布局,如果宽度不够会自动换行。
定位方案,坐标值(left,top)
Normal--定位基于基本布局过程,从左到右,从上到下递增实现。
Float--脱离正常文档流,居最左或居最右。
Absolute--其位置信息由用户指定。
有了size(width,height),position(left,top),box type(display:none,inline,block..e.t.c)一个显示元素基本就最终确定了。
布局完成后,浏览器将结果渲染出来,最终我们看到了眼前的页面。
大家有什么问题或技术上的想法可以在此与大家分享,也可以加入前端爱好者QQ群(141999928)一起学习进步:
【幸凡前端技术交流群】
如果您觉得本文的内容对您的学习有所帮助,捐赠与共勉,支付宝(左)或微信(右)