原文链接:http://javascript.info/onload-ondomcontentloaded,translate with ❤️ by zhangbao.

HTML 页面有三个主要的生命周期事件:

  • DOMContentLoaded——浏览器完全加载了 HTML、DOM 树构建完毕,但像 <img> 这样的外部资源还没完全加载。

  • load——浏览器加载完了页面中的所有资源。

  • beforeunload/unload——当用户离开页面时。

每个事件都有用:

  • DOMContentLoaded 事件——DOM 树解析好了,所以处理器中可以查询 DOM 节点,初始化界面。

  • load 事件——页面中的外部资源加载完毕,我们可以得到图片大小(如果未在 HTML/CSS 中设置的话)等。

  • beforeunload/unload——当用户离开:我们可以借机询问用户是否保存了更改,然后再离开。

下面我们来详细讨论这些事件。

DOMContentLoaded

DOMContentLoaded 事件在 document 对象上触发。

而且,我们必须使用 addEventListener 去绑定这个事件:

  1. document.addEventListener('DOMcontentLoaded', ready);

例如:

  1. <script>
  2. function ready () {
  3. alert('DOM 树解析好了');
  4. // 图片还未完全加载(除非缓存了),所以得到的大小是 0x0
  5. alert(`图片大小: ${img.offsetWidth}x${img.offsetHeight}`);
  6. }
  7. document.addEventListener('DOMcontentLoaded', ready);
  8. </script>

上例中,DOMContentLoaded 事件是在文档加载完成后执行的,而不是等到页面加载完毕,引出 alert 出来的大小都是 0。

DOMContentLoaded 事件第一眼看起来非常简单,在 DOM 树解析完成后触发。但还是有一些特别之处的。

DOMContentLoaded 和脚本

当浏览器初始化 HTML 遇到 <script>...</script> 的时候,就不会继续解析 DOM 树了。而是停下来,首先执行脚本,因此 DOMContentLoaded 事件只会在所有脚本执行完成后,才触发。

外部脚本(通过 src 引入)的加载和执行,也将导致 DOM 树解析暂停。因为 DOMContentLoaded 也会等待这些外部脚本。

唯一的例外是添加了 asyncdefer 属性的外部脚本。它们告诉浏览器继续处理,不要等我。因此,用户可以在脚本加载完成之前看到页面,有助于提高页面性能。

async 脚本和 defer 脚本

asyncdefer 属性仅对引入外部脚步有效。如果是没有 src 属性的脚本,会被浏览器忽略。

它们告诉浏览器,它会与页面一起工作,现在在“后台”加载,加载完成后就会执行。因此,这类脚本不会阻塞 DOM 树的解析和页面的呈现。

下表列出了它们的区别:

async defer
顺序 谁先加载完谁就先执行,跟它们出现在文档里的顺序无关。 总是按照在文档中的出现顺序执行。
DOMContentLoaded async 脚本可能会在文档没有完全下载完就执行了。当脚本很小或者有缓存、或者文档很大时就存在这种情况。 脚本会在文档完全加载和解析完毕后(如果需要就等)、刚好就在 DOMContentLoaded 之前执行。

所以,async 通常应用在完全独立的脚本上面。

DOMContentLoaded 和样式

外部样式表不会影响 DOM 解析,所以 DOMContentLoaded 事件不会等待它们。

这有一个陷阱:如果我们有一个依赖样式的脚本,那么这个脚本必须等待样式完成后才能执行:

  1. <link type="text/css" rel="stylesheet" href="style.css">
  2. <script>
  3. // 这个脚本不会等到样式加载完成后才执行
  4. alert(getComputedStyle(document.body).marginTop);
  5. </script>

原因是,脚本可能想要获得元素坐标和其他依赖于样式的属性,比如说上面的例子。自然地,它必须等待样式加载完成后。

现在,DOMContentLoaded 既要等待脚本完成,也需要等待样式完成了。

内置浏览器填充

FireFox、Chrome 和 Opera 在 DOMContentLoaded 时会自动填充表单。

例如,页面里包含登录名和密码的表单,浏览器记住了这些值,那么在 DOMContentLoaded 的时候就会尝试填充它们(如果用户同意的话)。

所以,如果 DOMContentLoaded 因为长脚本加载延迟了,那么自动填充也将等待。您可能在一些站点上看到了(如果您使用浏览器自动填充的话)——登录/密码字段不会立即自动填充,而是有延迟。这实际上是等到 DOMContentLoaded 事件发生的延迟。

使用外部 asyncdefer 脚本的一个小小好处是,它们不会阻塞 DOMContentLoaded 事件,因此也不会推迟浏览器的自动填充。

window.onload

当整个页面中的样式、图片和其他外部资源都加载完成后,就会触发 window 对象上的 load 事件。

下例中,显示了正确的图片尺寸,因为 window.onload 会等待所有图片加载完成:

  1. <script>
  2. window.onload = function() {
  3. alert('页面加载完成');
  4. // 这时图片也已经加载结束了
  5. alert(`图片尺寸: ${img.offsetWidth}x${img.offsetHeight}`);
  6. };
  7. </script>
  8. <img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">

window.onunload

当访问者离开页面时,会触发 window 对象上的 unload 事件。我们可以做一些不导致延迟的事情,比如关闭相关的弹出窗口,但是我们不能取消向另一个页面的转换。

如果想要取消到另一个页面的转换,需要使用另一个事件——onbeforeunload

window.onbeforeunlaod

如果访问者启动了离开页面的导航,或者试图关闭窗口,那么 beforeunload 事件处理程序将进行额外的确认。

它可能会返回一个带有问题的字符串,历史上的浏览器曾经显示过, 但到现在为止, 只有一些浏览器显示了它。这是因为某些网站管理员滥用此事件处理程序, 显示误导性和黑客性消息。

您可以通过运行这段代码并重新加载页面来尝试它。

  1. window.addEventListener('beforeunload', function(e) {
  2. return '您有未保存修改,确认离开吗?';
  3. });

readyState

那么在文档加载完毕后,我们设置 DOMContentLoaded 处理器会发生什么呢?

自然不会执行。

有些情况下,我们不确定文档是否已经准备好,例如一个带有 async 属性加载并运行的外部脚本。根据网络情况的不同,它可以在文档完成之前加载和执行,或者之后,我们不能确定。因此,我们应该知道文档的当前状态。

document.readyState 属性会给我们关于文档状态的信息。它有三个可能的取值:

  • loading”——文档正在加载。

  • interactive”——文档被完全阅读。

  • complete” ——文档被完全阅读并且所有资源(像图片)也都加载了。

我们可以检查 document.readyState 并设置一个处理程序,如果准备好了,就立即执行代码。

像这样:

  1. function work() { /*...*/ }
  2. if (document.readyState == 'loading') {
  3. document.addEventListener('DOMContentLoaded', work);
  4. } else {
  5. work();
  6. }

当状态改变的时候,就会触发 readyState 事件。我们可以像下面这样打印所有状态:

  1. // 当前状态
  2. console.log(document.readyState);
  3. // 打印状态改变
  4. document.addEventListener('readystatechange', () => console.log(document.readyState));

readystatechange 事件是跟踪文档加载状态的另一种可选机制,它在很久以前就出现了。现在,它很少被使用,但为了完整性我们在这里来讨论它。

readystatechange 与其他事件相比,是处于真个事件链中的哪一步呢?

为了清晰地看到计时信息,这有一个包含