从一条语句说浏览器页面渲染机制

1.引子

最近看到有道题讨论document.write在html文件中不同位置时在页面上的执行顺序(题目在后面具体讨论),于是写这篇博客讨论一下浏览器的页面渲染,在http请求中传输的字节码如何变成浏览器呈现在用户面前的界面。

2.进程与线程

这似乎是个永恒的话题,很多人开始都是混淆的。对于这两个概念,可以记住下面这几条:

  • 进程是cpu资源分配的最小单位(系统会给它分配内存)
  • 进程之间相互独立,对于浏览器,每打开一个Tab页都可以认为开了一个新的进程。
  • 进程拥有自己的多线程,各个线程之间相互协作完成任务。

在这里我们只讨论一个页面的渲染机制,即下面说的线程都在一个进程内。对于浏览器,开一个新页面(进程)工作的是以下线程:

 

 与浏览器页面渲染有关的主要是和这里的GUI引擎线程和JS引擎线程:

GUI引擎线程(后面我们说 UI线程):解析html css,进行 DOMCSSOMRenderTree的绘制,回流,重绘的执行者,以及页面渲染都是由他来完成(难以避免的抛出一大堆概念,后面会一一解释)。
  • JS引擎线程:用来对js文件进行处理。
  • 上面两个线程是互斥的(请记住这句话,很重要),当有一个在进行时,另外一个将被挂起,也就是说会造成阻塞,至于谁阻塞谁,后面再说。

3.当浏览器接收到服务器发过来的数据包时...

  • 将数据包进行解析。
  • 解析html文件,UI线程进行DOM树的构建,此操作将确定节点的父子以及兄弟关系。
  • 当继续解析到类似的<link rel='stylesheet' href='../example.css'/>语句时将下载相应的css文件,并进行CSSOM的构建(类似DOM的东西),将确定css属性之间的级联关系。
  • 浏览器将DOMCSSOM进行合并,构建RenderTree,所谓的渲染树。然后浏览器会根据渲染树进行名为reflow(回流)的过程,来根据浏览器页面的具体情况确定各个节点的渲染位置(该操作会遍历整个DOMCSSOM,对性能影响很大)。
  • 接下来就是将准备好的东西渲染到屏幕上了。以上过程可以用下面这张图演示

上面的看似很顺畅的过程,却隐去了一个重大的问题(js呢)?

  • 接上面谈起,浏览器开始解析时碰到<link rel='stylesheet' href='../example.css'/>会开始下载css文件,同样当遇到语句时<script src='./example.js'></script>js引擎会下载并执行js文件,注意这里会引起阻塞,阻塞具体情况如下:

    • 阻断DOM的构建,应为浏览器不知道js文件会对DOM进行什么操作,也就是说JS执行会阻塞DOM构建
    • 如果CSSOM没有就绪,那么JS将等到CSSOM准备就绪时再执行,也就是说CSSOM的构建会阻塞JS执行,其实也就是间接的在阻塞DOM的构建
  • 那么我们是否能对JS的执行进行操作呢,答案是:可以.
    • async属性<script src='./example.js' async></script>
      • 它的作用是指定相应的js文件在下载好再进行执行,也就是说这个js文件在下载过程中是不阻塞DOM构建的。
    • defer属性<script src='./example.js' defer></script>
      • 指定对应js文件在整个页面都解析完成后再执行,此时DOMCSSOM都已经准备就绪。

说一组概念(很重要):

  • 回流(reflow)和重绘(repaint)
    • 回流:当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档。
    • 重绘:当页面中元素样式的改变并不影响它在文档流中的位置时,浏览器会将新样式赋予给元素并重新绘制它。
    • 至于具体什么操作会引起回流以及重绘,这里不一一列出。
    • 回流必定重绘,重绘不一定回流。
    • 很明显:回流的代价要比重绘高很多。在性能优化时有一点就是避免频繁造成回流。

4.那么回到我们开篇的问题

先来介绍这条语句:

  • document.write
将一个文本字符串写入由 document.open() 打开的一个文档流。
注意: 因为 document.write 需要向 文档流中写入内容,因此在关闭(已加载)的文档上调用 document.write 会自动调用 document.open这将清空该文档的内容

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script>
        document.write('脚本输出');
    </script>
</head>
<body>
    <p>页面内容</p>
    <p>页面内容</p>
</body>
</html>复制代码

页面输出结果:

//脚本输出
//页面内容
//页面内容复制代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <p>页面内容</p>
    <script>document.write('脚本输出');</script>    <p>页面内容</p>
</body>
</html>复制代码

页面输出结果:

//页面内容
//脚本输出
//页面内容复制代码

以上的结果看似都是情理之中,document.write会在执行到时将内容添加到DOM树中。

而当我们把这条语句放到</body>或者</html>之后时,浏览器的做法都是将这条语句提升到</body>之后执行,下面是谷歌还有火狐,IE的截图。

  • chrome


  • firefox


  • IE


还有就是将上面的document.write放在onload时执行,像下面这样:

<!DOCTYPE html><html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script>
        window.onload=function(){
            document.write('脚本输出');
        }
    </script>
</head>
<body>
    <p>页面内容</p>
    <p>页面内容</p>
</body>
</html>复制代码

结果就是,不论script标签在哪,页面上都只会渲染脚本输出一句话,所以我们得到下面结论:

  • 浏览器解析DOMbody标签为止
  • body标签之后的script会被提升到</body>之前执行,但仍在onload事件之前。

后记:

根据个人理解以及整理得,有错误或者偏差敬请原谅,欢迎指正。


猜你喜欢

转载自juejin.im/post/5c56e54af265da2d9808d7bb