# 网络请求-资源加载
# 页面生命周期
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;
};
# 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 用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。