1. 影响浏览器渲染方式的文档模式
每个浏览器都有自己的页面渲染引擎。渲染引擎包括两部分:一部分负责 HTML、CSS 代码的解析(渲染引擎,如 blink 引擎 or 内核),一部分负责 JavaScript 代码的解析(JavaScript 引擎,如 V8 引擎)。浏览器的渲染模式(以何种文档模式进行渲染)主要对 CSS 解析有影响(也对脚本有一些影响)。不同的渲染模式,在 CSS 解析上存在差异,比如对盒模型的处理。
不同的渲染模式是历史遗留问题造成的。早期 W3C 没统一标准,浏览器生产商自己决定页面如何渲染;标准出来后,现有浏览器渲染肯定存在与标准不同的地方。为了兼容,就出现了两种浏览器渲染模式(IE 最早提出),正统叫法为文档模式(Document Mode)(如果文档是按照标准编写的,浏览器采用标准渲染模式;如果文档并没有按照标准编写,那么浏览器以怪异模式渲染)。此外,还有第三种模式—近标准模式。那么浏览器又该如何知道文档有没有按照标准编写呢?实际上浏览器在渲染页面之前会检查两个内容,一个是页面是否有 DOCTYPE
信息,另外一个是页面是否有 x-ua-compatible
信息。DOCTYPE
告诉浏览器:我的文档是哪种模式,你确定后,就按这种模式渲染;如果没有这个头信息,浏览器就按怪异模式渲染。x-ua-compatible
是 IE8 的一个专有属性,可以指定浏览器以怎样的模式进行渲染。
注意:浏览器模式和浏览器渲染模式是两个概念,前者可以理解为IE浏览器中特有的概念,后者在本文中就是指文档模式。
2. HTML 文档的解析过程
当用户在浏览器键入某网站地址,网站首页文档 index.html
加载完成后,浏览器开始解析 HTML。下文根据不同的 HTML 资源结构分析解析过程。
2.1 纯 HTML 文档,无 CSS 和脚本
如果 HTML 文档中只有 HTML,没有 CSS 和脚本的话,问题极其简单。浏览器解析 HTML,构建 DOM 树,DOM 树构建完成后(触发 DOMContentLoaded
事件),构建 render 树,接着布局和绘制像素。
2.2 包含内联样式和内联脚本的 HTML 文档
如果 HTML 文档中存在内联样式和脚本,这个时候,问题变得稍微复杂一些。浏览器解析 HTML,构建 DOM 树,当解析到标签时,样式信息开始被解析,CSSOM 被构建,但是它并不会影响到 HTML 的解析和 DOM 树的构建。当 HTML 解析到
标签时,因为脚本有可能改变 DOM 内容,所以 HTML 的解析必须等到脚本执行完毕后再继续。脚本又有可能操作 CSSOM ,所以脚本必须等到 CSS 解析完毕后才能执行。确保此刻 CSS 解析完成,脚本被交到 JS 引擎手里,由 JS 引擎执行。当脚本执行完毕,HTML 继续解析,直到全部 HTML 解析完毕,DOM 树构建完成(触发
DOMContentLoaded
事件)。
注意:DOMContentLoaded
事件只和 HTML 的加载和解析有关,一旦 HTML 解析完成,这个事件就会被触发,不管此时还有没有CSS的解析、图片的下载或者异步脚本的加载和执行。DOM 树一旦构建完成,就会开始构建 render 树,并不管 CSS 是否解析完毕。如果构建 render 树的时候,CSS 还没有解析完成,那么 render 树会用占位符代替应该有的 CSSOM 节点,当该节点加载解析好后,再重新计算样式。
但是同步脚本的执行会阻塞 HTML 的解析,从而会影响到 DOMContentLoaded
事件的触发。同时又要注意,CSS 会阻塞 JS 脚本的执行,从而间接影响到 HTML 的解析和 DOMContentLoaded
事件的触发。
2.3 包含外部 CSS 和脚本的 HTML 文档
如果 HTML 文档中存在外联样式表和脚本,问题变得更复杂一点。HTML 文档加载完成后,浏览器首先扫描 HTML 文档,查看有哪些外部资源需要启动 network operation 来请求资源,并在 HTML 解析的同时,发送所有的请求。CSS 资源加载完毕后,会立即开始解析构建 CSSOM。(同步脚本加载完毕后,并不能立刻执行。)当 HTML 解析到标签,先确认脚本加载完毕了没,如果没,那得等;如果加载好了,还得看 CSS 解析好了没。如果没,那还得等;如果 CSS 解析好了,那就能把脚本交给 JS 引擎去执行了。当 JS 执行完毕,HTML 继续解析,DOM 继续构建,直到全部构建完成,
DOMContentLoaded
事件被触发。紧接着,就是构建 render 树。
如果脚本有async
属性,问题就又不一样了。async
属性默认该脚本不会影响到 DOM 内容,所以只要脚本下载完成,(相关)CSS 解析完毕,脚本立刻执行,不用等着 HTML 解析到标签再开始执行。同样,HTML 也不会等着脚本执行完毕再解析。仿佛两者看不到对方,只管做自己的事情就行了。
3. JS 解释器的工作原理
上文提到浏览器在解析 HTML 文档的时候,会把脚本交给 JS 引擎执行,那么 JS 引擎是如何执行脚本(evaluating script)的呢?
3.1 扫描全局变量,确定所有已声明的变量或函数名
你如果利用 chrome 控制台调试 JS 代码,这个过程是看不到的,但确实存在。JS 解释器对脚本进行全局扫描,结束后得到全局环境中的变量对象,此过程发生了变量声明提升和函数声明提升。所有变量都没被赋值,其值为 undefined
;函数声明提升还包括了函数体的提升。下图是个例子:
备注:你可能会好奇上面这个图是怎么回事,这里简要概述一下:黄色区块表示JS执行环境,白色表格代表变量对象键值对模型,表格左列为全局变量(变量对象中的key),说明当前全局环境中共有model
、octopus
、catView
和catListView
四个变量。
以后会专门写一篇文章介绍JS执行时的内存模型,帮助大家形象理解JS代码的运行机制,从而有助于理解作用域、执行环境、this指向、闭包、继承、原型链等抽象概念。
3.2 顺序执行所有语句
当 JS 解释器知道整个文件中都有哪些声明好的全局变量或函数后,就会开始顺序执行文件中的语句,当然是从第一行开始。如果是赋值语句,就执行赋值操作;如果是函数调用语句,就执行函数调用。
下图是 debugger 刚开始时变量的情况,很明显,刚刚被 JS 解释器点过名,还没有开始执行赋值操作。
当解释器移动到下一行代码时,这个变量也就被赋值,存储了数据。在本例中这个数据是个对象类型,有两个属性,其中一个是数组,另一个是空值。
脚本的最后是函数调用语句:
JS 解析器执行到这里,准备调用 octopus
的 init
方法。
当所有的语句执行完毕后,JS 解释器任务结束,主导权交到 HTML 解析器手中,浏览器继续解析 HTML 文档。
从上述过程,我们能看出浏览器解析渲染 HTML 文档是单线程的,除了发送外部资源请求的操作。
4. 总结
浏览器的工作原理是网站性能优化的基础知识。CSS 不会阻塞 HTML 的解析,但是会阻塞渲染,CSS 的解析会阻塞脚本的执行,而脚本会阻塞 HTML 的解析。