时间线、解析与渲染

1. 从document.write说起

使用 document.write 方法向文档写入元素,默认不会覆盖body中存在的元素,正常追加,然而在 window.onload 的事件处理函数中执行,会替换掉body中已存在的元素内容。为什么会发生这样的现象,产生这样现象的原理是什么?

2. 时间线

浏览器从页面加载开始那一刻,到整个页面加载完毕,整个过程中按顺序发生的总流程,就叫做时间线。

时间线大概分为以下几个过程(序号不代表执行顺序,有些是同时发生的)。

1. 生成document对象(#document)

从生成document对象开始,JS开始起作用,DOM的功能体开始起作用。

2. 解析文档,构建DOM树、CSS树

document.readyState = ‘loading’; // 加载中

a. 从html代码的第一行开始,浏览器从第一行阅读到最后一行。

就传统浏览器来说,这个过程实际上是不做任何事情的,只是在阅读。
解析文档的同时,浏览器构建DOM树。

b. 遇到link标签,开新的线程异步加载CSS外部文件,阅读style标签,生成CSSOM(CSS树)。

DOM树和CSS树是同时构建的。

c. 没有设置异步加载的SCRIPT,阻塞文档解析。

只要遇到没有设置异步(async、defer)的script标签或动态设置的scrpt标签,会阻 
塞文档解析。DOM和CSS树构建也会停止,等待JS脚本加载并且执行完毕,继续解析文档。

d. 设置异步加载的SCRIPT,异步加载JS脚本并且执行(async),不阻塞解析文档。

异步脚本中,不能直接使用document.write(),使用则会报错。
如果必须使用,可以写在window.onload的事件处理函数中。

异步script不能有依赖其他脚本的操作,不能有需要触发的操作(比如监听文档解析完 
成),可以执行网络检查、网络请求等操作。异步操作实际在项目中使用较少。

e. 解析文档时,遇到img标签,先解析节点。遇到src,创建加载线程,异步加载图片资源,不阻塞解析文档。

3. 文档解析完成

document.readyState = ‘interactive’; // 可以进行交互

a. 设置异步(defer)的script,JS脚本开始按照顺序执行。

设置async的标签,异步加载完毕并且直接执行。
设置defer的标签,异步加载完毕,等待文档解析完毕后,开始执行脚本。

b. 触发DOMContentLoaded事件,代表文档解析完成。

文档解析完成,不代表文档加载完毕,如果存在图片,图片可能还在异步加载。
解析完成,代表DOM结构已经生成,渲染树已经生成。

程序从同步的脚本执行阶段向事件驱动阶段演化,用户交互在文档解析完成之后。

4. 文档解析完成、异步资源加载完毕

document.readyState = ‘complete’; // 文档加载完毕

async script加载并执行完毕,img资源加载完毕,window.onload事件被触发。

async的script标签,可能在文档解析完成后才开始加载。
async的和defer不一定谁先加载,都是异步执行。

所有的异步加载完毕,window.onload才被触发,之前一直在阻塞中。

3. 页面加载的三个阶段

1. 解析文档,构建DOM树开始。

document.readyState = ‘loading’; // 加载中

2. 文档解析完成

document.readyState = ‘interactive’; // 解析完成

3. window.onload触发,文档加载完成及资源加载完毕

document.readyState = ‘complete’; // 文档加载完成

文档状态改变触发该事件,监听的过程是由JS引擎完成的,不是用户,不能算是事件驱 
动阶段中一种。  

4. window.onload 和 DOMContentLoaded 区别?

DOMContentLoaded是文档解析完成后触发。
window.onload是文档解析完成,并且异步资源加载完成后触发,浪费时间。

5. 现代浏览器的布局和渲染

现代浏览器为了更好的用户体验,渲染引擎尝试尽快的的渲染到屏幕上,
先解析的部分先构建CSS树、DOM树和渲染树,即读到哪一部分,就开始渲染哪一部分。
现代浏览器是在解析的过程中就在执行渲染,一边解析一边渲染。
所以script标签应该放在底部,否则会浪费解析script文档的时间。

first paint(初次绘制),只要解析到html中需要渲染的东西,一边解析一边构建一边渲染。
如果把script放在顶部,初次渲染的时间就被延迟,所以可能存在页面留白的情况。不利于用户体验。

DOMContentLoaded:当renderTree全部渲染完毕之后,触发此事件。

6. 封装文档解析完毕函数

**
 * 判断文档解析完毕
 * 
 * @param {*} fn 
 */
function domReady (fn) {
  if (document.addEventListener) {
    document.addEventListener('DOMContentLoaded', function () {
      document.removeEventListener('DOMContentLoaded', arguments.callee, false);
      fn();
    }, false);
  } else if (document.attachEvent) {
    document.attachEvent('onreadystatechange', function () {
      if (this.readyState === 'complete') {
        document.detachEvent('onreadystatechange', arguments.callee);
        fn();
      }
    })
  } 

  // 判断不在iframe中、兼容IE67
  if (document.documentElement.doScroll && 
        typeof(window.frameElement) === 'undefined') {
          
    try {
      document.documentElement.doScroll('left');
    } catch (e) {
      return setTimeout(arguments.callee, 20);
    }

    fn();
  }
}