原文链接:http://javascript.info/onload-ondomcontentloaded,translate with ❤️ by zhangbao.
HTML 页面有三个主要的生命周期事件:
DOMContentLoaded
——浏览器完全加载了 HTML、DOM 树构建完毕,但像<img>
这样的外部资源还没完全加载。load
——浏览器加载完了页面中的所有资源。beforeunload/unload
——当用户离开页面时。
每个事件都有用:
DOMContentLoaded
事件——DOM 树解析好了,所以处理器中可以查询 DOM 节点,初始化界面。load
事件——页面中的外部资源加载完毕,我们可以得到图片大小(如果未在 HTML/CSS 中设置的话)等。beforeunload/unload
——当用户离开:我们可以借机询问用户是否保存了更改,然后再离开。
下面我们来详细讨论这些事件。
DOMContentLoaded
DOMContentLoaded
事件在 document
对象上触发。
而且,我们必须使用 addEventListener
去绑定这个事件:
document.addEventListener('DOMcontentLoaded', ready);
例如:
<script>
function ready () {
alert('DOM 树解析好了');
// 图片还未完全加载(除非缓存了),所以得到的大小是 0x0
alert(`图片大小: ${img.offsetWidth}x${img.offsetHeight}`);
}
document.addEventListener('DOMcontentLoaded', ready);
</script>
上例中,DOMContentLoaded
事件是在文档加载完成后执行的,而不是等到页面加载完毕,引出 alert
出来的大小都是 0。
DOMContentLoaded
事件第一眼看起来非常简单,在 DOM 树解析完成后触发。但还是有一些特别之处的。
DOMContentLoaded 和脚本
当浏览器初始化 HTML 遇到 <script>...</script>
的时候,就不会继续解析 DOM 树了。而是停下来,首先执行脚本,因此 DOMContentLoaded
事件只会在所有脚本执行完成后,才触发。
外部脚本(通过 src
引入)的加载和执行,也将导致 DOM 树解析暂停。因为 DOMContentLoaded
也会等待这些外部脚本。
唯一的例外是添加了 async
和 defer
属性的外部脚本。它们告诉浏览器继续处理,不要等我。因此,用户可以在脚本加载完成之前看到页面,有助于提高页面性能。
async 脚本和 defer 脚本
async
和 defer
属性仅对引入外部脚步有效。如果是没有 src
属性的脚本,会被浏览器忽略。
它们告诉浏览器,它会与页面一起工作,现在在“后台”加载,加载完成后就会执行。因此,这类脚本不会阻塞 DOM 树的解析和页面的呈现。
下表列出了它们的区别:
async |
defer |
|
---|---|---|
顺序 | 谁先加载完谁就先执行,跟它们出现在文档里的顺序无关。 | 总是按照在文档中的出现顺序执行。 |
DOMContentLoaded |
async 脚本可能会在文档没有完全下载完就执行了。当脚本很小或者有缓存、或者文档很大时就存在这种情况。 |
脚本会在文档完全加载和解析完毕后(如果需要就等)、刚好就在 DOMContentLoaded 之前执行。 |
所以,async
通常应用在完全独立的脚本上面。
DOMContentLoaded 和样式
外部样式表不会影响 DOM 解析,所以 DOMContentLoaded
事件不会等待它们。
这有一个陷阱:如果我们有一个依赖样式的脚本,那么这个脚本必须等待样式完成后才能执行:
<link type="text/css" rel="stylesheet" href="style.css">
<script>
// 这个脚本不会等到样式加载完成后才执行
alert(getComputedStyle(document.body).marginTop);
</script>
原因是,脚本可能想要获得元素坐标和其他依赖于样式的属性,比如说上面的例子。自然地,它必须等待样式加载完成后。
现在,DOMContentLoaded
既要等待脚本完成,也需要等待样式完成了。
内置浏览器填充
FireFox、Chrome 和 Opera 在 DOMContentLoaded
时会自动填充表单。
例如,页面里包含登录名和密码的表单,浏览器记住了这些值,那么在 DOMContentLoaded
的时候就会尝试填充它们(如果用户同意的话)。
所以,如果 DOMContentLoaded
因为长脚本加载延迟了,那么自动填充也将等待。您可能在一些站点上看到了(如果您使用浏览器自动填充的话)——登录/密码字段不会立即自动填充,而是有延迟。这实际上是等到 DOMContentLoaded
事件发生的延迟。
使用外部 async
和 defer
脚本的一个小小好处是,它们不会阻塞 DOMContentLoaded
事件,因此也不会推迟浏览器的自动填充。
window.onload
当整个页面中的样式、图片和其他外部资源都加载完成后,就会触发 window
对象上的 load
事件。
下例中,显示了正确的图片尺寸,因为 window.onload
会等待所有图片加载完成:
<script>
window.onload = function() {
alert('页面加载完成');
// 这时图片也已经加载结束了
alert(`图片尺寸: ${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
事件。我们可以做一些不导致延迟的事情,比如关闭相关的弹出窗口,但是我们不能取消向另一个页面的转换。
如果想要取消到另一个页面的转换,需要使用另一个事件——onbeforeunload
。
window.onbeforeunlaod
如果访问者启动了离开页面的导航,或者试图关闭窗口,那么 beforeunload
事件处理程序将进行额外的确认。
它可能会返回一个带有问题的字符串,历史上的浏览器曾经显示过, 但到现在为止, 只有一些浏览器显示了它。这是因为某些网站管理员滥用此事件处理程序, 显示误导性和黑客性消息。
您可以通过运行这段代码并重新加载页面来尝试它。
window.addEventListener('beforeunload', function(e) {
return '您有未保存修改,确认离开吗?';
});
readyState
那么在文档加载完毕后,我们设置 DOMContentLoaded
处理器会发生什么呢?
自然不会执行。
有些情况下,我们不确定文档是否已经准备好,例如一个带有 async
属性加载并运行的外部脚本。根据网络情况的不同,它可以在文档完成之前加载和执行,或者之后,我们不能确定。因此,我们应该知道文档的当前状态。
document.readyState
属性会给我们关于文档状态的信息。它有三个可能的取值:
“
loading
”——文档正在加载。“
interactive
”——文档被完全阅读。“
complete
” ——文档被完全阅读并且所有资源(像图片)也都加载了。
我们可以检查 document.readyState
并设置一个处理程序,如果准备好了,就立即执行代码。
像这样:
function work() { /*...*/ }
if (document.readyState == 'loading') {
document.addEventListener('DOMContentLoaded', work);
} else {
work();
}
当状态改变的时候,就会触发 readyState
事件。我们可以像下面这样打印所有状态:
// 当前状态
console.log(document.readyState);
// 打印状态改变
document.addEventListener('readystatechange', () => console.log(document.readyState));
readystatechange
事件是跟踪文档加载状态的另一种可选机制,它在很久以前就出现了。现在,它很少被使用,但为了完整性我们在这里来讨论它。
readystatechange
与其他事件相比,是处于真个事件链中的哪一步呢?
为了清晰地看到计时信息,这有一个包含
<script>
function log(text) { console.log(Date.now(), text) }
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="/pen/?editors=1012" onload="log('iframe onload')"></iframe>
<img src="http://en.js.cx/clipart/train.gif" id="img">
<script>
img.onload = () => log('img onload');
</script>
会类似打印这样的输出:
[1] initial readyState:loading
[2] readyState:interactive
[2] DOMContentLoaded
[3] img onload
[4] iframe onload
[4] readyState:complete
[4] window onload
方括号中的数字表示它触发时的大致时间。实际时间稍微大一些,但是用相同数字标记的事件大约在同一时间发生(上下几毫秒的时差)。
document.readyState
的interactive
刚好在DOMContentLoaded
之前。这两个事件实际上是相同的。当所有资源(
img
和iframe
)加载完毕后,document.readyState
的状态变为complete
。可以看到它与iframe.onload
是在同一时间(iframe
是最后的资源)。complete
状态和window.onload
是一样的。window.onload
事件总在最后触发。
总结
页面生命周期事件:
DOMContentLoaded
事件在document
对象上触发,当 DOM 树结构解析好以后。到这一步就可以操作页面中的元素了。除了
async
和defer
之外的外部脚本都会执行。图片和其他资源可能仍在继续加载。
当页面中的所有资源都加载完成后,会触发
window
对象的load
事件。我们很少使用它,因为没必要等那么长时间。当用户离开页面的时候,会触发
window
对象的beforeunload
事件。事件处理器中return
一个字符串,浏览器询问访问者是否确认离开页面。当用户最终离开页面的时候,会触发
window
对象的unload
事件。在事件处理器中,我们只能做些简单的事情,不涉及延误或询问一个用户。因为有这个限制,所以很少使用。document.readyState
的值反映了当前的文档状态,可以在readyStateChange
事件中追踪状态变化:“
loading
”——文档正在加载。“
interactive
”—— 文档已解析,与DOMContentLoaded
事件发生在同一时间,但是在DOMContentLoaded
之前。“
complete
” ——文档和文档资源加载完毕,与window.onload
同一时间发生,但是在window.onload
之前。
(完)