# 网络请求-资源加载

# 页面生命周期

HTML 页面的生命周期包含三个重要事件:

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

每个事件都是有用的:

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

# DOMContentLoaded

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

    // 图片目前尚未加载完成(除非已经被缓存),所以图片的大小为 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 处理程序在文档加载完成后触发,所以它可以查看所有元素,包括它下面的 元素。

但是,它不会等待图片加载。因此,alert 显示其大小为零。

# window.onload

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

下面的这个示例正确显示了图片大小,因为 window.onload 会等待所有图片加载完毕:

<script>
  window.onload = function () {
    // 与此相同 window.addEventListener('load', (event) => {
    alert("Page loaded");

    // 此时图片已经加载完成
    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

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

有一个值得注意的特殊情况是发送分析数据。

案例

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

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

有一个特殊的 navigator.sendBeacon(url, data) 方法可以满足这种需求

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

let analyticsData = {
  /* 带有收集的数据的对象 */
};

window.addEventListener("unload", function () {
  navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});

需要注意的是:

  • 请求以 POST 方式发送。
  • 我们不仅能发送字符串,还能发送表单以及其他格式的数据。
  • 数据大小限制在 64kb

如果我们要取消跳转到另一页面的操作,在这里做不到。但是我们可以使用另一个事件 —— onbeforeunload。

# window.onbeforeunload

如果访问者触发了离开页面的导航(navigation)或试图关闭窗口,beforeunload 处理程序将要求进行更多确认。

如果我们要取消事件,浏览器会询问用户是否确定。

你可以通过运行下面这段代码,然后重新加载页面来进行尝试:

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

1

# readyState

如果我们在文档加载完成之后设置 DOMContentLoaded 事件处理程序,会发生什么?

很自然地,它永远不会运行。

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

document.readyState 属性可以为我们提供当前加载状态的信息。

它有 3 个可能值:

  • loading —— 文档正在被加载。
  • interactive —— 文档被全部读取。
  • complete —— 文档被全部读取,并且所有资源(例如图片等)都已加载完成。

function work() {
  console.log("work");
}

if (document.readyState == "loading") {
  // 仍在加载,等待事件
  document.addEventListener("DOMContentLoaded", work);
} else {
  // DOM 已就绪!
  work();
}

# 小结

页面生命周期事件:

  • 当 DOM 准备就绪时,document 上的 DOMContentLoaded 事件就会被触发。在这个阶段,我们可以将 JavaScript 应用于元素。
  • 当页面和所有资源都加载完成时,window 上的 load 事件就会被触发。我们很少使用它,因为通常无需等待那么长时间。
  • 当用户想要离开页面时,window 上的 beforeunload 事件就会被触发。如果我们取消这个事件,浏览器就会询问我们是否真的要离开(例如,我们有未保存的更改)。
  • 当用户最终离开时,window 上的 unload 事件就会被触发。在处理程序中,我们只能执行不涉及延迟或询问用户的简单操作。正是由于这个限制,它很少被使用。我们可以使用 navigator.sendBeacon 来发送网络请求。
  • document.readyState 是文档的当前状态,可以在 readystatechange 事件中跟踪状态更改

# async / defer

现代的网站中,脚本往往比 HTML 更“重”:它们的大小通常更大,处理时间也更长。

当浏览器加载 HTML 时遇到 <script>...</script> 标签,浏览器就不能继续构建 DOM。它必须立刻执行此脚本。对于外部脚本<script src="..."></script> 也是一样的:浏览器必须等脚本下载完,并执行结束,之后才能继续处理剩余的页面。

这会导致两个重要的问题:

  • 脚本不能访问到位于它们下面的 DOM 元素,因此,脚本无法给它们添加处理程序等。
  • 如果页面顶部有一个笨重的脚本,它会“阻塞页面”。在该脚本下载并执行结束前,用户都不能看到页面内容:

# defer

defer 特性告诉浏览器不要等待脚本。相反,浏览器将继续处理 HTML,构建 DOM。脚本会“在后台”下载,然后等 DOM 构建完成后,脚本才会执行。

换句话说:

  • 具有 defer 特性的脚本不会阻塞页面。
  • 具有 defer 特性的脚本总是要等到 DOM 解析完毕,但在 DOMContentLoaded 事件之前执行。

还有两点需要注意:

  • 具有 defer 特性的脚本保持其相对顺序(文档顺序),就像常规脚本一样。只是被执行延后了。
  • defer 特性仅适用于外部脚本

# async

async 特性与 defer 有些类似。它也能够让脚本不阻塞页面。但是,在行为上二者有着重要的区别。

async 特性意味着脚本是完全独立的:

  • 浏览器不会因 async 脚本而阻塞
  • 其他脚本不会等待 async 脚本加载完成,同样,async 脚本也不会等待其他脚本。
  • DOMContentLoaded 和异步脚本不会彼此等待

换句话说,async 脚本会在后台加载,并在加载就绪时运行。DOM 和其他脚本不会等待它们,它们也不会等待其它的东西。async 脚本就是一个会在加载完成时执行的完全独立的脚本。

使用场景

当我们将独立的第三方脚本集成到页面时,此时采用异步加载方式是非常棒的:计数器,广告等,因为它们不依赖于我们的脚本,我们的脚本也不应该等待它们

# 动态脚本

在下面这个例子中,loadScript(src) 函数添加了一个脚本,并将 async 设置为了 false。

因此,long.js 总是会先执行(因为它是先被添加到文档的):

function loadScript(src) {
  let script = document.createElement("script");
  script.src = src;
  script.async = false;
  document.body.append(script);
}

// long.js 先执行,因为代码中设置了 async=false
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");

如果没有 script.async=false,脚本则将以默认规则执行,即文档顺序(small.js 的 script 标签在上方)。

# 小结

在实际开发中,defer 用于需要整个 DOM 的脚本,和/或脚本的相对执行顺序很重要的时候。

async 用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。