页面生命周期:DOMContentLoaded ,loaded, beforeUnloaded, unload
HTML的页面生命周期包括三个重要事件:
DOMContentLoaded:浏览器已经完全加载HTML,并构建了DOM树,但是像img、css等外部资源还未完全加载loaded:浏览器不仅加载完了HTML,还加载完成了所有的外部资源:图片、样式等beforeUnloaded/unload:当用户正在离开页面
每个事件都是有用的:
DOMContentLoaded:DOM已就绪,这时可以查找DOMloaded:外部资源已加载完成,样式也被应用,这时图片的大小也已知了beforeUnloaded:当用户真正离开页面之前被调用,可以询问用户是否要离开unload:用户真的离开了页面,但是我们还是可以做一些操作,如发送统计数据
一、DOMContentLoaded
document.addEventListener("DOMContentLoaded", function)
DOMContentLoaded 和 脚本
<script>脚本是阻塞的
当浏览器处理一个HTML文档,并在文档中遇到<script>脚本是阻塞的标签时,会停止构建DOM,转而运行<script>中的脚本,这是因为<script>脚本可以想要修改DOM,所以DOMContentLoaded会等待<script>脚本执行结束后再调用。
因此,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>
在上面这个例子中,我们首先会看到 “Library loaded…”,然后才会看到 “DOM ready!”(所有脚本都已经执行结束)。
非阻塞的脚本
此规则有两个例外:
- 具有
async特性(attribute)的脚本不会阻塞DOMContentLoaded,稍后 我们会讲到。 - 使用
document.createElement('script')动态生成并添加到网页的脚本也不会阻塞DOMContentLoaded。
DOMContentLoaded与样式
外部样式表不会影响DOM,因此DOMContentLoaded也不会等待样式。
但这里有个陷阱:如果样式表后面有一个script脚本,那么script脚本会等待样式表加载完成再执行。
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// 在样式表加载完成之前,脚本都不会执行
alert(getComputedStyle(document.body).marginTop);
</script>
原因是:脚本可能想获取元素的坐标或其他的样式,因此它必须等待样式表加载完成,当DOMContentLoaded在等待脚本的同时,它也在等待样式表的加载
浏览器内建的自动填充
Firefox,Chrome 和 Opera 都会在 DOMContentLoaded 中自动填充表单。
例如,如果页面有一个带有登录名和密码的表单,并且浏览器记住了这些值,那么在 DOMContentLoaded 上,浏览器会尝试自动填充它们(如果得到了用户允许)。
因此,如果 DOMContentLoaded 被需要加载很长时间的脚本延迟触发,那么自动填充也会等待。你可能在某些网站上看到过(如果你使用浏览器自动填充)—— 登录名/密码字段不会立即自动填充,而是在页面被完全加载前会延迟填充。这实际上是 DOMContentLoaded 事件之前的延迟。
二、window.onload
当整个页面、包括样式、图片等外部资源全部加载完成时会触发load事件,可以通过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 事件就会被触发。我们可以在那里做一些不涉及延迟的操作,例如关闭相关的弹出窗口。
四、window.onbeforeunload
如果访问者触发了离开页面的导航(navigation)或试图关闭窗口,beforeunload 处理程序将要求进行更多确认。
如果我们要取消事件,浏览器会询问用户是否确定。
你可以通过运行下面这段代码,然后重新加载页面来进行尝试:
window.onbeforeunload = function() {
return false;
};
readyState
如果我们在文档加载完成之后设置 DOMContentLoaded 事件处理程序,会发生什么?
很自然地,它永远不会运行。
在某些情况下,我们不确定文档是否已经准备就绪。我们希望我们的函数在 DOM 加载完成时执行,无论现在还是以后。
document.readyState 属性可以为我们提供当前加载状态的信息。
它有 3 个可能值:
loading—— 文档正在被加载。interactive—— 文档被全部读取。complete—— 文档被全部读取,并且所有资源(例如图片等)都已加载完成。
所以,我们可以检查 document.readyState 并设置一个处理程序,或在代码准备就绪时立即执行它。
像这样:
function work() { /*...*/ }
if (document.readyState == 'loading') {
// 仍在加载,等待事件
document.addEventListener('DOMContentLoaded', work);
} else {
// DOM 已就绪!
work();
}
还有一个 readystatechange 事件,会在状态发生改变时触发,因此我们可以打印所有这些状态,就像这样:
// 当前状态
console.log(document.readyState);
// 状态改变时打印它
document.addEventListener('readystatechange', () => console.log(document.readyState))
脚本:async,defer
当浏览器加载 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>
<!-- 直到脚本执行结束才显示在页面上 -->
<p>...content after script...</p>
解决办法:将脚本放在页面底部,此时,它可以访问到它上面的元素,并且不会阻塞页面显示内容。
但是这种解决方案远非完美。例如,浏览器只有在下载了完整的 HTML 文档之后才会注意到该脚本(并且可以开始下载它)。对于长的 HTML 文档来说,这样可能会造成明显的延迟。
这对于使用高速连接的人来说,这不值一提,他们不会感受到这种延迟。但是这个世界上仍然有很多地区的人们所使用的网络速度很慢,并且使用的是远非完美的移动互联网连接。
幸运的是,这里有两个 <script> 特性(attribute)可以为我们解决这个问题:defer 和 async。
defer (延迟、推迟)
defer特性会告诉浏览器不要等待脚本执行,相反,浏览器会继续处理HTML,构建DOM。脚本会在后台下载,然后等到DOM构建完成后,脚本才会执行
<p>...content before script...</p>
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<!-- 立即可见 -->
<p>...content after script...</p>
换句话说:
- 具有
defer特性的脚本不会阻塞页面 - 具有
defer特性的脚本总是要等到DOM解析完毕,但是在DOMContentLoaded事件之前执行
下面这段代码解释了第二句话:
<p>...content before scripts...</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 scripts...</p>
- 页面内容立即显示。
DOMContentLoaded事件处理程序等待具有defer特性的脚本执行完成。它仅在脚本下载且执行结束后才会被触发。
具有 defer 特性的脚本保持其相对顺序,就像常规脚本一样。
假设,我们有两个具有 defer 特性的脚本:long.js 在前,small.js 在后。
<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>
浏览器扫描页面寻找脚本,然后并行下载它们,以提高性能。因此,在上面的示例中,两个脚本是并行下载的。small.js 可能会先下载完成。
……但是,defer 特性除了告诉浏览器“不要阻塞页面”之外,还可以确保脚本执行的相对顺序。因此,即使 small.js 先加载完成,它也需要等到 long.js 执行结束才会被执行。
当我们需要先加载 JavaScript 库,然后再加载依赖于它的脚本时,这可能会很有用。
defer特性仅适用于外部脚本
如果
<script>脚本没有src,则会忽略defer特性。
async(异步脚本)
async特性和defer有些类似,都不会阻塞页面。但有个重大的差别
defer脚本是按文档顺序的,DOMContentLoaded会等待defer脚本执行结束
async脚本是按加载优先顺序的,先下载完成先执行,DOMContentLoaded和async脚本是相互独立的,不会等待
DOMContentLoaded可能会发生在异步脚本之前(如果异步脚本在页面完成后才加载完成)DOMContentLoaded也可能发生在异步脚本之后(如果异步脚本很短,或者是从 HTTP 缓存中加载的)
换句话说,async 脚本会在后台加载,并在加载就绪时运行。DOM 和其他脚本不会等待它们,它们也不会等待其它的东西。async 脚本就是一个会在加载完成时执行的完全独立的脚本。就这么简单,现在明白了吧?
当我们将独立的第三方脚本集成到页面时,此时采用异步加载方式是非常棒的:计数器,广告等,因为它们不依赖于我们的脚本,我们的脚本也不应该等待它们:
<!-- Google Analytics 脚本通常是这样嵌入页面的 -->
<script async src="https://google-analytics.com/analytics.js"></script>
async特性仅适用于外部脚本
就像
defer一样,如果<script>标签没有src特性(attribute),那么async特性会被忽略
动态脚本
此外,还有一种向页面添加脚本的重要的方式。
我们可以使用 JavaScript 动态地创建一个脚本,并将其附加(append)到文档(document)中:
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)
当脚本被附加到文档 (*) 时,脚本就会立即开始加载。
默认情况下,动态脚本的行为是“异步”的。
也就是说:
- 它们不会等待任何东西,也没有什么东西会等它们。
- 先加载完成的脚本先执行(“加载优先”顺序)。
如果我们显式地设置了 script.async=false,则可以改变这个规则。然后脚本将按照脚本在文档中的顺序执行,就像 defer 那样。
在下面这个例子中,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 大概会先执行)。
总结
async 和 defer 有一个共同点:加载这样的脚本都不会阻塞页面的渲染。因此,用户可以立即阅读并了解页面内容。
但是,它们之间也存在一些本质的区别:
| 顺序 | DOMContentLoaded | |
|---|---|---|
async | 加载优先顺序。脚本在文档中的顺序不重要 —— 先加载完成的先执行 | 不相关。可能在文档加载完成前加载并执行完毕。如果脚本很小或者来自于缓存,同时文档足够长,就会发生这种情况。 |
defer | 文档顺序(它们在文档中的顺序) | 在文档加载和解析完成之后(如果需要,则会等待),即在 DOMContentLoaded 之前执行。 |
在实际开发中,defer 用于需要整个 DOM 的脚本,和/或脚本的相对执行顺序很重要的时候。
async 用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。
同样,和 defer 一样,如果我们要加载一个库和一个依赖于它的脚本,那么顺序就很重要。
资源加载:onload,onerror
浏览器允许我们跟踪外部资源的加载 —— 脚本,iframe,图片等。
这里有两个事件:
onload—— 成功加载,onerror—— 出现 error。