页面生命周期

HTML 页面加载时的生命周期分为以下四个重要事件:

  • DOMContentLoaded——浏览器已加载 HTML,并构建 DOM 树,但 css 之类的外部资源可能没加载完成。(此时可以访问 DOM 节点)
  • load——浏览器不仅加载完成 HTML,还加载完成了所有外部资源:图片、样式等。(此时外部资源全部加载完成,可以获取样式、大小等)
  • beforeunload——用户正在离开页面时。(我们可以询问用户是否已保存)
  • unload——用户几乎已经离开了。(依然可以启动一些操作,比如发送统计数据等)

DOMContentLoaded 生命周期

我们可以通过document来监听DOMContentLoaded事件

html
1<script>
2  function ready() {
3    alert('DOM is ready')
4
5    // 图片目前尚未加载完成(除非已经被缓存),所以图片的大小为 0x0
6    alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`)
7  }
8
9  document.addEventListener('DOMContentLoaded', ready)
10</script>
11
12<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0" />

DOMContentLoaded在文档加载完就触发了,此时由于 img 图片并没有被获取到(外部资源没有加载完成),而仅仅只能获取到 DOM 接口,所以上面的代码并不能打印出 img 的大小。

DOMContentLoaded的逻辑非常简单:不等外部样式,而是 DOM 树构建完成就触发。

但是事实并非如此简单,我们在实际开发中不可能保证 DOM 树构建完成而外部资源没有获取吧?以下介绍一些细节:

DOMContentLoaded 触发条件细节

DOMContentLoaded 和 script

当浏览器处理一个 HTML 文档,并在文档中遇到 <script> 标签时,就会在继续构建 DOM 之前运行它。

这是因为 script 内的代码可能会修改 DOM,所以必须先运行它。

而DOMContentLoaded是在构建完 DOM 树之后才会启动的事件,所以 DOMContentLoaded 肯定会等以下脚本全部加载完成后才执行:

html
1<script>
2  document.addEventListener('DOMContentLoaded', () => {
3    alert('DOM ready!')
4  })
5</script>
6
7<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>
8
9<script>
10  alert('Library loaded, inline script executed')
11</script>

在上面的例子中,一定是先打印"Library loaded, inline script executed"这句话,才会打印"DOM ready!"

这个规则有两个例外:

1.当 script 有 async 属性

2.script 是由 document.createElement('script')动态生成并加入到网页中时

DOMContentLoaded 会提前生效,不会被阻塞

DOMContentLoaded 和外部样式

外部样式表不会影响 DOM,因此 DOMContentLoaded 不会等待它们。

但这里有一个陷阱。如果在样式后面有一个脚本,那么该脚本必须等待样式表加载完成:

html
1<link type="text/css" rel="stylesheet" href="style.css" />
2<script>
3  // 在样式表加载完成之前,脚本都不会执行
4  alert(getComputedStyle(document.body).marginTop)
5</script>

这是因为浏览器是自上而下解析并执行的,当遇到 link 时,就会去解析外部样式文件,然后再执行 script 标签内的代码。

当 script 标签在等样式表加载完成时,DOMContentLoaded 等 script 标签加载完。

所以DOMContentLoaded也会等待外部资源加载完成,对吧?

DOMContentLoaded 的应用

Firefox,Chrome 和 Opera 都会在 DOMContentLoaded 中自动填充表单。

例如,如果页面有一个带有登录名和密码的表单,并且浏览器记住了这些值,那么在 DOMContentLoaded 上,浏览器会尝试自动填充它们(如果得到了用户允许)。

因此,如果 DOMContentLoaded 被需要加载很长时间的脚本延迟触发,那么自动填充也会等待。

load 生命周期

当整个页面,包括样式、图片、其他资源等被加载完成后,就会触发 window 对象的 load事件,可以通过window.onload属性来获取此事件

javascript
1<script>
2  window.onload = function() { // 与此相同 window.addEventListener('load', (event) => {
3    alert('Page loaded');
4
5    // 此时图片已经加载完成
6    alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
7  };
8</script>
9
10<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">

上面代码中,等到图片加载完成后,才会触发 onload 事件,此时会打出图片的属性。

beforeunload 生命周期

当用户触发了离开页面的导航(或者关闭页面的按钮),这个生命周期就会执行,我们可以在这个生命周期做以下操作让浏览器询问用户是否真的要离开

javascript
1window.onbeforeunload = function () {
2  return false
3}

unload 生命周期

当用户离开页面时,其实浏览器还可以帮我们运行代码,这部分代码会在window.onunload属性中被调用。我们可以在这里做不涉及延迟的操作。

这里比较特殊的情况是发送统计分析数据。

当用户离开页面时,我们希望通过 unload 事件来发送数据给服务器。

这里有特殊的 API 可以满足这种需求

navigator.sendBeacon(url,data)方法可以在后台发送数据,并且在另一个页面上不会有延迟:浏览器离开页面时,依然在执行 sendBeacon

javascript
1let analyticsData = {
2  /* 带有收集的数据的对象 */
3}
4
5window.addEventListener('unload', function () {
6  navigator.sendBeacon('/analytics', JSON.stringify(analyticsData))
7})

这个 API 做了以下事情:

  • 请求是以 post 方式发送的
  • 大小限制在 64k
  • 可以发送表单/字符串/其他格式的数据
  • 有一个 keep-alive标志,这个标志为 true 时表示即使已经离开了页面,浏览器也必须执行请求

当然如果用 fetch也可以实现,只要实现上面的要求即可

javascript
1window.onunload = function () {
2  fetch('/analytics', {
3    method: 'POST',
4    body: 'statistics',
5    keepalive: true,
6  })
7}

这里有一个坑,浏览器对此实现并不好,可能有 bug 导致统计数据缺失。所以 MDN 里已经不推荐这样做了,具体可以看这篇文档

Beacon API is broken

readyState

有一个确定文档处于哪种生命周期的 API——通过访问 document.readyState可以提供加载状态的信息

它有三个值

  • loading 文档正在被加载
  • interactive 文档全部读取,此时 触发 DOMContentLoaded 事件
  • complete 文档读取完成,并且所有资源(图片/外部样式)都加载完成 此时触发 load 事件

readyState 的改变可以通过 readyStatechange来监听

javascript
1// 状态改变时打印它
2document.addEventListener('readystatechange', () => console.log(document.readyState))

通过以下代码可以看到 readyState 和生命周期的顺序关系

javascript
1    <script>
2      const { log } = console;
3      log("initial readyState:" + document.readyState);
4
5      document.addEventListener("readystatechange", () =>
6        log("readyState:" + document.readyState)
7      );
8      document.addEventListener("DOMContentLoaded", () =>
9        log("DOMContentLoaded")
10      );
11
12      window.onload = () => log("window onload");
13    </script>
14   //测试时这里需要新建一个 iframe 文件
15    <iframe src="iframe.html" onload="console.log('iframe onload')"></iframe>
16    <img src="http://en.js.cx/clipart/train.gif" id="img" />
17    <script>
18      img.onload = () => console.log("img onload");
19    </script>

打印结果是这样的

bash
1initial readyState:loading
2readyState:interactive
3DOMContentLoaded
4iframe onload
5img onload
6readyState:complete
7window onload
  1. 刚开始时,解析整个文档,还没生成 dom 树,此时为 loading
  2. 当生成 dom 树后,此时 readyState 改变,触发 DOMContented 生命周期
  3. 加载 iframe/img 里的资源,完成样式和外部资源的加载
  4. readState 改变 complete,触发 load 生命周期

小结

页面生命周期事件如下:

  • 当 DOM 树生成后,会进入 DOMContentLoaded 生命周期,此时DOMContentLoaded 事件会触发:
    • 这个事件的代码即使一开始就被执行了,也会等待其他script执行结束才调用
    • 图片和其他资源继续被加载
  • 当页面和其他资源被加载完成后,load事件触发
  • 用户想要离开页面时,会进入beforeunload 生命周期,此时返回一个 false,浏览器会询问用户是否真的要离开
  • 用户已经离开页面,那么会进入unload生命周期,此时可以发送一些统计数据等不涉及延迟的操作
  • 可以通过document.readyStateChange事件捕获document.readyState,从而获知页面加载状态