2022 年 10 月 14 日

页面:DOMContentLoaded、load、beforeunload、unload

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

  • DOMContentLoaded - 浏览器完全加载了 HTML,并且 DOM 树已构建,但外部资源(如图片 <img> 和样式表)可能尚未加载。
  • load - 不仅加载了 HTML,还加载了所有外部资源:图像、样式等。
  • beforeunload/unload - 用户正在离开页面。

每个事件都可能很有用

  • DOMContentLoaded 事件 - DOM 已就绪,因此处理程序可以查找 DOM 节点,初始化界面。
  • load 事件 - 外部资源已加载,因此应用了样式,已知图像大小等。
  • beforeunload 事件 - 用户正在离开:我们可以检查用户是否已保存更改,并询问他们是否真的要离开。
  • unload - 用户几乎离开了,但我们仍然可以启动一些操作,例如发送统计信息。

让我们探讨这些事件的详细信息。

DOMContentLoaded

DOMContentLoaded 事件发生在 document 对象上。

我们必须使用 addEventListener 来捕获它

document.addEventListener("DOMContentLoaded", ready);
// not "document.onDOMContentLoaded = ..."

例如

<script>
  function ready() {
    alert('DOM is ready');

    // image is not yet loaded (unless it was cached), so the size is 0x0
    alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
  }

  document.addEventListener("DOMContentLoaded", ready);
</script>

<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">

在示例中,当文档加载时,DOMContentLoaded 处理程序运行,因此它可以看到所有元素,包括下面的 <img>

但它不会等待图像加载。因此 alert 显示零大小。

乍一看,DOMContentLoaded 事件非常简单。DOM 树已就绪 - 这是事件。不过有一些特点。

DOMContentLoaded 和脚本

当浏览器处理 HTML 文档并遇到 <script> 标记时,它需要在继续构建 DOM 之前执行。这是一个预防措施,因为脚本可能希望修改 DOM,甚至 document.write 到其中,因此 DOMContentLoaded 必须等待。

因此 DOMContentLoaded 肯定发生在这样的脚本之后

<script>
  document.addEventListener("DOMContentLoaded", () => {
    alert("DOM ready!");
  });
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>

<script>
  alert("Library loaded, inline script executed");
</script>

在上面的示例中,我们首先看到“库已加载……”,然后看到“DOM 已就绪!”(所有脚本都已执行)。

不会阻止 DOMContentLoaded 的脚本

此规则有两个例外

  1. 具有 async 属性的脚本(我们将在 稍后介绍)不会阻止 DOMContentLoaded
  2. 使用 document.createElement('script') 动态生成的脚本,然后添加到网页中也不会阻止此事件。

DOMContentLoaded 和样式

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

但有一个陷阱。如果我们在样式之后有一个脚本,那么该脚本必须等到样式表加载

<link type="text/css" rel="stylesheet" href="style.css">
<script>
  // the script doesn't execute until the stylesheet is loaded
  alert(getComputedStyle(document.body).marginTop);
</script>

原因是脚本可能希望获取元素的坐标和其他依赖样式的属性,如上面的示例所示。自然,它必须等待样式加载。

由于 DOMContentLoaded 等待脚本,因此它现在也在它们之前等待样式。

内置浏览器自动填充

Firefox、Chrome 和 Opera 在 DOMContentLoaded 上自动填充表单。

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

因此,如果 DOMContentLoaded 被长时间加载的脚本推迟,则自动填充也会等待。您可能在某些网站上看到过这种情况(如果您使用浏览器自动填充)——登录/密码字段不会立即自动填充,但页面完全加载之前会有延迟。这实际上是 DOMContentLoaded 事件之前的延迟。

window.onload

当整个页面加载完毕,包括样式、图像和其他资源时,window 对象上的 load 事件触发。此事件可通过 onload 属性获得。

下面的示例正确显示图像大小,因为 window.onload 等待所有图像

<script>
  window.onload = function() { // can also use window.addEventListener('load', (event) => {
    alert('Page loaded');

    // image is loaded at this time
    alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
  };
</script>

<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">

window.onunload

当访问者离开页面时,unload 事件在 window 上触发。我们可以做一些不涉及延迟的事情,例如关闭相关的弹出窗口。

值得注意的例外是发送分析数据。

假设我们收集有关页面如何使用的数据:鼠标点击、滚动、查看的页面区域等。

自然地,unload 事件是用户离开我们的时候,我们希望将数据保存在我们的服务器上。

规范 https://w3c.github.io/beacon/ 中描述了一种用于此类需求的特殊 navigator.sendBeacon(url, data) 方法。

它在后台发送数据。不会延迟到另一个页面的转换:浏览器离开页面,但仍执行 sendBeacon

以下是使用方法

let analyticsData = { /* object with gathered data */ };

window.addEventListener("unload", function() {
  navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});
  • 请求作为 POST 发送。
  • 我们不仅可以发送字符串,还可以发送表单和其他格式,如 Fetch 章节中所述,但通常它是一个字符串化的对象。
  • 数据限制为 64kb。

sendBeacon 请求完成时,浏览器可能已经离开了文档,因此无法获取服务器响应(对于分析来说通常是空的)。

对于通用网络请求,fetch 方法中还有一个 keepalive 标志,用于执行此类“页面离开后”请求。您可以在 Fetch API 章节中找到更多信息。

如果我们想取消到另一个页面的转换,我们不能在这里这样做。但我们可以使用另一个事件——onbeforeunload

window.onbeforeunload

如果访问者发起离开页面的导航或尝试关闭窗口,则 beforeunload 处理程序会要求进行额外的确认。

如果我们取消事件,浏览器可能会询问访问者是否确定。

您可以通过运行此代码然后重新加载页面来尝试

window.onbeforeunload = function() {
  return false;
};

出于历史原因,返回非空字符串也被视为取消事件。一段时间前,浏览器通常将其显示为消息,但正如 现代规范 所述,它们不应该这样做。

以下是一个示例

window.onbeforeunload = function() {
  return "There are unsaved changes. Leave now?";
};

此行为已更改,因为一些网站管理员滥用此事件处理程序,显示误导性和烦人的消息。因此,现在旧浏览器仍可能将其显示为消息,但除此之外,无法自定义向用户显示的消息。

event.preventDefault() 不适用于 beforeunload 处理程序

这听起来可能很奇怪,但大多数浏览器都忽略 event.preventDefault()

这意味着,以下代码可能无法正常工作

window.addEventListener("beforeunload", (event) => {
  // doesn't work, so this event handler doesn't do anything
  event.preventDefault();
});

相反,在这样的处理程序中,应该将 event.returnValue 设置为字符串,以获得与上述代码类似的结果

window.addEventListener("beforeunload", (event) => {
  // works, same as returning from window.onbeforeunload
  event.returnValue = "There are unsaved changes. Leave now?";
});

readyState

如果我们在加载文档后设置 DOMContentLoaded 处理程序,会发生什么情况?

当然,它永远不会运行。

在某些情况下,我们不确定文档是否已准备好。我们希望我们的函数在 DOM 加载时执行,无论现在还是以后。

document.readyState 属性告诉我们当前的加载状态。

有 3 个可能的值

  • "loading" – 文档正在加载。
  • "interactive" – 文档已完全读取。
  • "complete" – 文档已完全读取,并且所有资源(如图像)也已加载。

因此,我们可以检查 document.readyState,并在其准备就绪时设置处理程序或立即执行代码。

如下所示

function work() { /*...*/ }

if (document.readyState == 'loading') {
  // still loading, wait for the event
  document.addEventListener('DOMContentLoaded', work);
} else {
  // DOM is ready!
  work();
}

还有 readystatechange 事件,当状态更改时触发,因此我们可以像这样打印所有这些状态

// current state
console.log(document.readyState);

// print state changes
document.addEventListener('readystatechange', () => console.log(document.readyState));

readystatechange 事件是跟踪文档加载状态的另一种机制,它很早以前就出现了。如今,它很少被使用。

让我们看看完整的事件流以了解其完整性。

这里有一个带有 <iframe><img> 和记录事件的处理程序的文档

<script>
  log('initial readyState:' + document.readyState);

  document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
  document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded'));

  window.onload = () => log('window onload');
</script>

<iframe src="iframe.html" onload="log('iframe onload')"></iframe>

<img src="https://en.js.cx/clipart/train.gif" id="img">
<script>
  img.onload = () => log('img onload');
</script>

工作示例 在沙盒中

典型输出

  1. [1] 初始 readyState:loading
  2. [2] readyState:interactive
  3. [2] DOMContentLoaded
  4. [3] iframe onload
  5. [4] img onload
  6. [4] readyState:complete
  7. [4] window onload

方括号中的数字表示它发生的大致时间。标记有相同数字的事件大约在同一时间 (± 几毫秒) 发生。

  • document.readyStateDOMContentLoaded 之前立即变为 interactive。这两件事实际上意味着相同的事情。
  • 当所有资源(iframeimg)加载完毕时,document.readyState 变为 complete。在这里,我们可以看到它发生在与 img.onloadimg 是最后一个资源)和 window.onload 大致相同的时间。切换到 complete 状态与 window.onload 相同。不同之处在于,window.onload 始终在所有其他 load 处理程序之后工作。

总结

页面加载事件

  • 当 DOM 准备就绪时,DOMContentLoaded 事件在 document 上触发。我们可以在此阶段将 JavaScript 应用于元素。
    • 诸如 <script>...</script><script src="..."></script> 的脚本会阻止 DOMContentLoaded,浏览器会等待它们执行。
    • 图像和其他资源也可能继续加载。
  • 当页面和所有资源加载完毕时,window 上的 load 事件触发。我们很少使用它,因为通常无需等待这么长时间。
  • 当用户想要离开页面时,window 上的 beforeunload 事件触发。如果我们取消事件,浏览器会询问用户是否真的想要离开(例如,我们有未保存的更改)。
  • 当用户最终离开时,window 上的 unload 事件触发,在处理程序中,我们只能执行不涉及延迟或询问用户的一些简单操作。由于该限制,它很少被使用。我们可以使用 navigator.sendBeacon 发送网络请求。
  • document.readyState 是文档的当前状态,可以在 readystatechange 事件中跟踪更改
    • loading – 文档正在加载。
    • interactive – 文档已解析,发生在与 DOMContentLoaded 大致相同的时间,但早于它。
    • complete – 文档和资源已加载,发生在与 window.onload 大致相同的时间,但早于它。
教程地图

评论

在评论之前阅读此内容…
  • 如果您有改进建议,请提交 GitHub 问题或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容,请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行代码,请将其包装在 <pre> 标记中,对于 10 行以上的代码,请使用沙盒 (plnkrjsbincodepen…)