🥡 JavaScript 文档和资源加载
# 。页面生命周期
HTML 页面的生命周期包含三个重要事件:
DOMContentLoaded
:浏览器已完全加载 HTML,并构建了 DOM 树,但像<img>
和样式表之类的外部资源可能尚未加载完成。load
:浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。beforeunload
/unload
: 当用户正在离开页面时。
每个事件的详细用途:
DOMContentLoaded
事件:DOM 已经就绪,因此处理程序可以查找 DOM 节点,并初始化接口。load
事件: 外部资源已加载完成,样式已被应用,图片大小也已知了。beforeunload
事件:用户正在离开,可以检查用户否保存了更改,并询问他是否真的要离开。unload
:用户几乎已经离开了,但是仍然可以启动一些操作,例如发送统计数据。
# DOMContentLoaded
DOMContentLoaded
事件发生在 document
对象上,必须使用 addEventListener
来捕获它:
document.addEventListener("DOMContentLoaded", ready);
🌰 例子:
<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">
2
3
4
5
6
7
8
9
10
11
12
DOMContentLoaded
处理程序在文档加载完成后触发,所以它可以查看所有元素,包括它下面的<img>
元素。但是,它不会等待图片加载。因此,
alert
显示其大小为零。
# 脚本
当浏览器处理一个 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>
2
3
4
5
6
7
8
9
10
11
所以先显示
Library loaded
,再显示DOM ready
。
提示
两个例外:
- 具有
async
特性(attribute)的脚本不会阻塞DOMContentLoaded
; - 使用
document.createElement('script')
动态生成并添加到网页的脚本也不会阻塞DOMContentLoaded
。
# 样式
外部样式表不会影响 DOM,因此 DOMContentLoaded
不会等待它们。但是如果样式后面有一个脚本,那么该脚本必须等待样式表加载完成:
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// 在样式表加载完成之前,脚本都不会执行
alert(getComputedStyle(document.body).marginTop);
</script>
2
3
4
5
脚本可能想要获取元素的坐标和其他与样式相关的属性,如上例所示。因此,它必须等待样式加载完成。
所以,当
DOMContentLoaded
等待脚本时,它现在也在等待脚本前面的样式。
# 浏览器表单自动填充
🌰 例子:
如果页面有一个带有登录名和密码的表单,并且浏览器记住了这些值,那么在 DOMContentLoaded
上,浏览器会尝试自动填充它们(如果得到了用户允许)。
如果
DOMContentLoaded
被需要加载很长时间的脚本延迟触发,那么自动填充也会等待。某些网站中,登录名 / 密码字段不会立即自动填充,而是在页面被完全加载前会延迟填充。这实际上是
DOMContentLoaded
事件之前的延迟。
# window.onload
当整个页面,包括样式、图片和其他资源被加载完成时,会触发 window
对象上的 load
事件。可以通过 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">
2
3
4
5
6
7
8
9
10
这个例子会正确显示了图片大小,因为
window.onload
会等待所有图片加载完毕。
# window.onunload
当访问者离开页面时, window
对象上的 unload
事件就会被触发。可以在那里做一些不涉及延迟的操作,例如关闭相关的弹出窗口。
特殊情况:发送分析数据:假设我们收集有关页面使用情况的数据:鼠标点击,滚动,被查看的页面区域等。当用户要离开的时候,我们希望通过 unload
事件将数据保存到我们的服务器上。
特殊的 navigator.sendBeacon(url, data)
方法可以满足这种需求,它在后台发送数据,转换到另外一个页面不会有延迟:浏览器离开页面,但仍然在执行 sendBeacon
。
🌰 例子:
let analyticsData = { /* 带有收集的数据的对象 */ };
window.addEventListener("unload", function() {
navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});
2
3
4
5
- 请求以 POST 方式发送。
- 不仅能发送字符串,还能发送表单以及其他格式的数据。
- 数据大小限制在 64kb。
当 sendBeacon
请求完成时,浏览器可能已经离开了文档,所以就无法获取服务器响应。
还有一个 keep-alive
标志,该标志用于在 fetch (opens new window) 方法中为通用的网络请求执行此类 “离开页面后” 的请求。
如果要取消跳转到另一页面的操作,以使用另一个事件 onbeforeunload
。
, 浏览器内对 unload 内使用 fetch 等 api 发送统计数据的实现并不好,可能有 bug 导致统计数据失准。所以 MDN 里已经不推荐这样做了 https://volument.com/blog/s... (opens new window)
# window.onbeforeunload
如果访问者触发了离开页面的导航或试图关闭窗口, beforeunload
处理程序将要求进行更多确认。
如果要取消事件,浏览器会询问用户是否确定。
# readyState
在某些情况下,不确定文档是否已经准备就绪。如果希望的函数在 DOM 加载完成时执行,无论现在还是以后。
document.readyState
属性可以为提供当前加载状态的信息。三个可能的值:
loading
:文档正在被加载。interactive
:文档被全部读取。complete
:文档被全部读取,并且所有资源(例如图片等)都已加载完成。
🌰 例子:
function work() { /*...*/ }
if (document.readyState == 'loading') {
// 仍在加载,等待事件
document.addEventListener('DOMContentLoaded', work);
} else {
// DOM 已就绪!
work();
}
2
3
4
5
6
7
8
9
检查
document.readyState
并设置一个处理程序,或在代码准备就绪时立即执行它。
readystatechange
事件,会在状态发生改变时触发,因此可以打印所有这些状态:
// 当前状态
console.log(document.readyState);
// 状态改变时打印它
document.addEventListener('readystatechange', () => console.log(document.readyState));
2
3
4
5
readystatechange
事件是跟踪文档加载状态的另一种机制,它很早就存在了。现在则很少被使用。
# 生命周期总结
页面的生命周期事件:
DOMContentLoaded
事件: DOM 准备就绪时事件就会被触发。在这个阶段,可以将 JavaScript 应用于元素。- 诸如
<script>...</script>
或<script src="..."></script>
之类的脚本会阻塞DOMContentLoaded
,浏览器将等待它们执行结束。 - 图片和其他资源仍然可以继续被加载。
- 诸如
window.onload
事件:当页面和所有资源都加载完成时触发。window.beforeunload
事件:在用户想要离开页面时。常用于要取消这个事件,浏览器就会询问我们是否真的要离开(例如,有未保存的更改)。window.unload
事件:处理程序中,只能执行不涉及延迟或询问用户的简单操作。正是由于这个限制,它很少被使用。可以使用navigator.sendBeacon
来发送网络请求。
获取当前文档的状态,使用 document.readyState
。可以在 readystatechange
事件中跟踪状态更改:
loading
:文档正在被加载。interactive
:文档已被解析完成,与DOMContentLoaded
几乎同时发生,但是在DOMContentLoaded
之前发生。complete
:文档和资源均已加载完成,与window.onload
几乎同时发生,但是在window.onload
之前发生。
# 脚本加载类型 / 顺序
在现代的网站中,脚本往往体量很大,需要长事件处理。
当浏览器加载 HTML 时遇到 <script>...</script>
标签,浏览器就不能继续构建 DOM。它必须立刻执行此脚本。对于外部脚本 <script src="..."></script>
也是一样的:浏览器必须等脚本下载完,并执行结束,之后才能继续处理剩余的页面。
这导致了两个问题:
** 脚本不能访问它完成加载之前下面的 DOM 元素。** 因此脚本无法给它们添加处理程序等。
如果页面顶部加载了一个大型的脚本,这会阻塞页面。在脚本下载和执行结束之前,用户都不能看见页面内容。
<p>...content before script...</p> <script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script> <!-- This isn't visible until the script loads --> <p>...content after script...</p>
1
2
3
4
5
6
解决方法:将脚本放在页面的底部。这样脚本可以访问元素的同时,不会阻塞页面的加载。
但是这种解决方案远非完美。例如,浏览器只有在下载了完整的 HTML 文档之后才会注意到该脚本(并且可以开始下载它)。对于长的 HTML 文档来说,这样可能会造成明显的延迟。
<script>
的两个特性可以解决这两个问题: defer
/ async
# defer
这个特性用于告诉浏览器 不要等待脚本的加载。浏览器可以继续处理 DOM,构建 DOM,同时脚本在 「后台下载」,等 DOM 构建完成后,脚本才会执行。
🌰 例子:
<p>...content before script...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<!-- 立即可见 -->
<p>...content after script...</p>
2
3
4
5
6
7
8
9
10
使用 defer
的 <script>
:
脚本不会阻塞页面的加载;
脚本总是等到 DOM 解析完毕,但是在
DOMContentedLoaded
事件之前执行。具有
defer
特性的脚本保持其相对顺序,就像常规脚本一样。例如两个
defer
特性的脚本:<script defer src="https://javascript.info/article/script-async-defer/long.js"></script> <script defer src="https://javascript.info/article/script-async-defer/small.js"></script>
1
2浏览器会扫描脚本,并行下载它们,以此提高性能,因此在下面的实例中,两个脚本是并行下载的,但是
defer
还会确保脚本执行的相对顺序。即使small.js
先加载完成,它也需要等到long.js
执行结束才会被执行。
defer
需要先加载 JavaScript 库,然后再加载依赖于它的脚本时,这可能会很有用。并且 ⚠️ defer
只能用于外部的脚本。(如果 <script>
脚本没有 src
,则会忽略 defer
特性。)
# async
async
特性与 defer
有些类似。它也能够让脚本不阻塞页面。但是在行为上二者有着重要的区别。
async
特性意味着脚本是完全独立的:
- 浏览器不会因
async
脚本而阻塞。 - 其他脚本不会等待
async
脚本加载完成,同样,async
脚本也不会等待其他脚本。 DOMContentLoaded
和异步脚本不会彼此等待:DOMContentLoaded
可能会发生在异步脚本之前(如果异步脚本在页面完成后才加载完成)DOMContentLoaded
也可能发生在异步脚本之后(如果异步脚本很短,或者是从 HTTP 缓存中加载的)
⚠️ async
脚本会在后台加载,在加载就绪时运行;但是 DOM 和其他脚本不会等待它们加载并且 它们也不会等待其他东西;意味着是一个在加载完成时执行的完全独立的脚本。
🌰 例子:
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>
<script async src="https://javascript.info/article/script-async-defer/long.js"></script>
<script async src="https://javascript.info/article/script-async-defer/small.js"></script>
<p>...content after scripts...</p>
2
3
4
5
6
7
8
9
10
这个例子中的 外部脚本的引入,不会等待对方的加载,先加载完成的(可能是
small.js
)先执行。并且:
- 页面内容立刻显示出来:加载写有
async
的脚本不会阻塞页面渲染。DOMContentLoaded
可能在async
之前或之后触发,不能保证谁先谁后。- 异步脚本以加载优先的顺序执行。
当使用 第三方脚本 集成到页面时,使用 异步脚本加载方式,可以不必等待它们加载就完成 DOM 的加载。与 defer
相同, async
特性仅适用于外部脚本。
# 动态脚本
这时一种向页面添加脚本的重要方式,可以使用 JavaScript 动态创建一个脚本,并将其附加到文档中。
🌰 例子:
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)
2
3
利用
document.createElement
,当脚本被附加到文档(*)
时,脚本就会立即开始加载。
在默认的情况下动态脚本的行为是 异步的。
- 不会等待任何东西,也会有东西等待它们;
- 先加载完成的脚本先执行(加载优先顺序)。
但是如果显式设置 script.async = false
就可以改变这个规则。脚本此时按照 defer
方式,脚本将按照脚本在文档中的顺序执行。
🌰 例子:
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");
2
3
4
5
6
7
8
9
10
# 总结
defer
一般用于需要整个 DOM 的脚本,和 / 或脚本的相对执行顺序很重要的时候。async
一般用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。
# 资源加载
浏览器允许跟踪外部资源的加载,加载资源相关有两个事件:
onload
:成功加载;onerror
:出现错误。
外部资源包括,图片、
iframe
、图片。
# 加载脚本
对于加载 第三方的脚本,调用其中的函数,可以动态加载:
let script = document.createElement('script');
script.src = "my.js";
document.head.append(script);
2
3
4
要运行该脚本中的函数,需要等待脚本的加载完成才能运行。
使用 自定义的脚本,现在一般使用 🪤JavaScript 的模块管理。第三方库依照使用说明导入。
# script.onlaod
onlaod
事件,在脚本加载并执行完成时触发。
🌰 例子:
let script = document.createElement('script');
// 可以从任意域(domain),加载任意脚本
script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"
document.head.append(script);
script.onload = function() {
// 该脚本创建了一个变量 "_"
alert( _.VERSION ); // 显示库的版本
};
2
3
4
5
6
7
8
9
10
在
onload
中可以使用脚本中的变量,运行函数。
# script.onerror
当脚本加载期间,出现的 error
会被 onerror
事件跟踪到。
🌰 例子:
let script = document.createElement('script');
script.src = "https://example.com/404.js"; // 没有这个脚本
document.head.append(script);
script.onerror = function() {
alert("Error loading " + this.src); // Error loading https://example.com/404.js
};
2
3
4
5
6
7
onerror
无法获取更多 HTTP error 的详细信息。不能知道是 404 还是 500 或者其他情况。只知道是加载失败了。
提示
onload
/ onerror
事件仅跟踪加载本身。在脚本处理和执行期间可能发生的 error 超出了这些事件跟踪的范围。如果脚本成功加载,则即使脚本中有编程 error,也会触发 onload
事件。如果要跟踪脚本 error,可以使用 window.onerror
全局处理程序。
# 其他资源加载
onload
/ onerror
事件也同样适用于 其他资源(基本上具有外部 src
的任何资源)的加载。
🌰 例子:
let img = document.createElement('img');
img.src = "https://js.cx/clipart/train.gif";
img.onload = function() {
alert(`Image loaded, size ${img.width}x${img.height}`);
};
img.onerror = function() {
alert("Error occurred while loading image");
};
2
3
4
5
6
7
8
9
10
11
提示
注意:
- 大多数资源在被添加到文档中后,便开始加载。但是
<img>
是个例外。它要等到获得 src 才开始加载。 - 对于
<iframe>
来说,iframe
加载完成时会触发iframe.onload
事件,无论是成功加载还是出现error
。