- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
本篇技术指南以 Modern JavaScript Tutorial 的 UI 章节《Document and resource loading》为核心,系统讲解 HTML 页面生命周期中的四个关键事件:DOMContentLoaded、load、beforeunload与unload,并辅以仓库内的可运行沙箱示例(readystate.view、window-onbeforeunload.view)验证真实事件时序。读完本文,你将掌握:何时初始化 DOM 交互、何时安全读取图片尺寸、如何在用户离开页面时上报统计,以及如何通过document.readyState写出「无论脚本何时执行都能正确初始化」的健壮代码。
页面生命周期:三个关键事件概述
一个 HTML 页面从开始加载到用户离开,其生命周期由三个重要事件串联:
DOMContentLoaded—— 浏览器已完成 HTML 的解析、DOM 树构建完毕,但外部资源(如<img>图片、样式表)可能尚未加载完成。load—— 不仅 HTML 加载完成,所有外部资源(图片、样式等)也已全部就绪。beforeunload/unload—— 用户正在离开页面。
每个事件都有明确的适用场景:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
DOMContentLoaded | DOM 就绪,外部资源可能未加载 | 查找 DOM 节点、初始化界面逻辑 |
load | 外部资源全部加载完成 | 读取已应用的样式、获取图片真实尺寸 |
beforeunload | 用户即将离开 | 检查未保存的更改并请求确认 |
unload | 用户几乎已离开 | 发起不阻塞的收尾操作,如发送统计 |
下面逐一深入这些事件的细节。
DOMContentLoaded:DOM 就绪的信号
DOMContentLoaded事件发生在document对象上,必须使用addEventListener捕获:
document.addEventListener("DOMContentLoaded", ready); // 不能写成 "document.onDOMContentLoaded = ..."原因在于DOMContentLoaded没有对应的onDOMContentLoaded属性,只能通过addEventListener注册。
看一个典型示例——在DOMContentLoaded触发时,虽然文档中的全部元素(包括下方的<img>)都已可见,但图片本身可能尚未加载,因此alert显示的尺寸为0x0:
<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很简单:DOM 树就绪就触发。但其中有几个容易忽略的细节。
DOMContentLoaded 与脚本的阻塞关系
浏览器解析 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>上述示例的执行顺序是:先看到 "Library loaded...",然后才是 "DOM ready!"(所有脚本都执行完毕后)。
注意:不阻塞 DOMContentLoaded 的脚本该规则存在两个例外:
- 带
async属性的脚本——详见 Scripts: async, defer 一章,它们不会阻塞DOMContentLoaded。- 通过
document.createElement('script')动态创建并插入页面的脚本,同样不会阻塞该事件。
关于第 1 点,2-ui/5-loading/02-script-async-defer/article.md 给出了完整的背景:当浏览器遇到<script src="...">时必须等它下载、执行完才能继续构建 DOM,这会导致「顶部的大脚本阻塞页面」。defer属性让浏览器在后台加载脚本、等 DOM 构建完成后再执行;async属性则让脚本加载完即执行、完全不等待 DOM。这些机制正是理解DOMContentLoaded何时触发的关键延伸。
DOMContentLoaded 与样式的间接依赖
外部样式表本身不影响 DOM,所以DOMContentLoaded并不会等待样式表。
但这里有一个陷阱:如果样式表后面紧跟一个脚本,那么这个脚本必须等样式表加载完成后才能执行:
<link type="text/css" rel="stylesheet" href="style.css"> <script> // 这个脚本要等样式表加载完成才执行 alert(getComputedStyle(document.body).marginTop); </script>原因很实际:脚本可能需要读取元素的坐标、以及其他依赖样式的属性(如上面例子中的getComputedStyle),自然必须等样式加载完毕。由于DOMContentLoaded要等待脚本,而脚本又要等待其前面的样式,DOMContentLoaded最终也间接等待了这些样式。
浏览器内置表单自动填充
Firefox、Chrome 和 Opera 会在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">与DOMContentLoaded示例形成对比:load会等待所有图片,所以此时读取img.offsetWidth得到的是真实尺寸。
window.onunload:离开前的最后收尾
当访问者离开页面时,unload事件在window上触发。我们可以在其中做一些不涉及延迟的操作,比如关闭关联的弹窗。
一个重要的例外场景是发送统计数据。假设我们要收集页面使用数据:鼠标点击、滚动、浏览区域等,用户离开时自然想把数据保存到服务器。
为此,规范(Beacon 规范,见 2-ui/5-loading/01-onload-ondomcontentloaded/article.md 引用的 w3c 标准)提供了专门的navigator.sendBeacon(url, data)方法。它会在后台发送数据,不会延迟页面跳转:浏览器虽然已离开页面,但仍会执行sendBeacon请求。
用法示例:
let analyticsData = { /* object with gathered data */ }; window.addEventListener("unload", function() { navigator.sendBeacon("/analytics", JSON.stringify(analyticsData)); });使用sendBeacon需要注意:
- 请求以POST方式发送。
- 不仅可以发送字符串,还可以发送表单等其他格式(详见 Fetch 一章),但通常发送的是序列化后的对象。
- 数据大小限制为 64KB。
sendBeacon请求结束时,浏览器很可能已经离开当前文档,因此无法获取服务器响应(统计接口的响应通常为空,影响不大)。
对于需要在「页面离开后」继续进行的通用网络请求,fetch方法还提供了keepalive标志。仓库中的 Fetch API 一章(第 194–223 行)对keepalive有完整说明:
- 正常文档卸载时,所有关联的网络请求会被中止;
keepalive: true告诉浏览器在后台执行请求,即使页面已经离开也能成功。 keepalive请求同样受64KB body 限制,且该限制作用于所有keepalive请求的总和——可以并行发起多个,但 body 长度之和不能超过 64KB。- 文档已卸载时无法处理服务器响应。
unload中无法取消跳转,但可以用另一个事件——onbeforeunload。
window.onbeforeunload:离开前的确认
当访问者发起离开页面的导航或尝试关闭窗口时,beforeunload处理器会请求额外确认。如果取消事件,浏览器可能会询问访问者是否确定离开。
试试运行以下代码后刷新页面:
window.onbeforeunload = function() { return false; };由于历史原因,返回非空字符串同样被视为取消事件。过去浏览器会把它作为消息展示给用户,但现代规范已不允许这样做:
window.onbeforeunload = function() { return "There are unsaved changes. Leave now?"; };行为变更的原因是:一些站长滥用该处理器展示误导性、烦人的消息。所以现在只有旧浏览器仍可能把字符串显示为消息,除此之外无法自定义展示给用户的消息内容。
警告:
beforeunload处理器中的event.preventDefault()不生效听起来可能很奇怪,但大多数浏览器会忽略event.preventDefault()。也就是说,下面这段代码可能不生效:window.addEventListener("beforeunload", (event) => { // 不生效,所以这个处理器什么也不做 event.preventDefault(); });正确的做法是:在这种处理器中设置
event.returnValue为字符串,才能获得与上面return方式类似的效果:window.addEventListener("beforeunload", (event) => { // 生效,与从 window.onbeforeunload 中 return 效果相同 event.returnValue = "There are unsaved changes. Leave now?"; });
仓库中提供了可交互的验证沙箱 window-onbeforeunload.view/index.html:页面包含一个「Set window.onbeforeunload」按钮用于动态设置window.onbeforeunload处理器,以及一个指向外部站点的链接用于触发导航离开。点击按钮后点击该链接,即可在浏览器中观察确认弹窗的触发行为。
document.readyState:查询当前加载状态
如果在文档加载完成之后才设置DOMContentLoaded处理器,它自然永远不会运行。但当不确定文档是否已就绪时,我们希望函数「无论现在还是稍后」都能在 DOM 加载后执行。
document.readyState属性告诉我们当前加载状态,共 3 个可能值:
| 值 | 含义 |
|---|---|
"loading" | 文档正在加载 |
"interactive" | 文档已完全解析读取 |
"complete" | 文档已完全解析,且所有资源(如图片)也已加载 |
所以可以检查document.readyState:仍在加载就注册DOMContentLoaded处理器,否则立即执行代码:
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));readystatechange是追踪文档加载状态的另一种机制,出现得较早,如今已很少使用。
完整事件流:readystate 沙箱验证
为了完整呈现事件的先后顺序,仓库提供了 readystate.view 沙箱:文档包含<iframe>、<img>,并为所有相关事件注册了日志处理器(对应文件为 readystate.view/iframe.html)。核心代码如下:
<script> 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="iframe.html" onload="log('iframe onload')"></iframe> <img src="https://en.js.cx/clipart/train.gif" id="img"> <script> img.onload = () => log('img onload'); </script>沙箱中使用performance.now()记录每个事件发生的毫秒时间(见 readystate.view/index.html 中的log函数),并填充大量隐藏文本来延长文档解析时间,以便更清晰地拉开事件间隔。
典型输出如下:
[1] initial readyState:loading[2] readyState:interactive[2] DOMContentLoaded[3] iframe onload[4] img onload[4] readyState:complete[4] window onload
方括号中的数字表示事件发生的大致时间,标有相同数字的事件在相近时刻发生(相差几毫秒)。
由此可以得出两个关键结论:
document.readyState变为interactive的时刻紧挨在DOMContentLoaded之前,这两者实际含义相同。document.readyState变为complete是在所有资源(iframe和img)加载完成之后,这里可以看到它和img.onload(最后一个资源)、window.onload几乎同时发生。切换到complete状态与window.onload含义相同,区别在于window.onload总是在其他所有load处理器之后运行。
总结:页面加载事件的完整速查
页面加载事件一览:
DOMContentLoaded—— DOM 就绪时在document上触发,此时可以对元素应用 JavaScript。<script>...</script>或<script src="..."></script>会阻塞DOMContentLoaded,浏览器要等它们执行完毕。- 图片等其他资源可能仍在加载中。
load—— 页面及所有资源加载完成时在window上触发。很少使用,因为通常没必要等那么久。beforeunload—— 用户想离开页面时在window上触发。如果取消事件,浏览器会询问用户是否确定离开(例如存在未保存的更改时)。unload—— 用户最终离开时在window上触发,处理器中只能做不涉及延迟、无需询问用户的简单操作,因此很少使用;可以通过navigator.sendBeacon发出网络请求。document.readyState—— 文档的当前状态,可通过readystatechange事件追踪变化:loading—— 文档正在加载。interactive—— 文档已解析完成,大约与DOMContentLoaded同时发生,但略早于它。complete—— 文档与资源均已加载,大约与window.onload同时发生,但略早于它。
实践建议:日常开发中,优先在DOMContentLoaded中初始化交互逻辑(查找节点、绑定事件、读取无需样式的属性);需要读取图片尺寸、样式相关数据时再用load;上报统计使用unload+navigator.sendBeacon(或fetch的keepalive: true);提示用户保存未提交更改使用beforeunload,并牢记event.returnValue而非preventDefault()。若脚本可能晚于文档加载执行,用document.readyState判断后决定立即执行还是等待DOMContentLoaded,即可写出时序无关的健壮初始化代码。
- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
相关推荐
Modern JavaScript Tutorial:深入掌握 async/await 异步编程
Modern JavaScript Tutorial:深入掌握 async/await 异步编程 导读 async/await 是 JavaScript 中基于
文档/教程前端掌控页面生命周期:PageLifecycle.js
掌控页面生命周期:PageLifecycle.js 在网页开发的世界里,了解页面的状态和其生命周期对于优化性能和用户体验至关重要。这就是为什么我们要向你推荐一个
前端JavaScript状态机生命周期事件全面解析
JavaScript状态机生命周期事件全面解析 引言:为什么需要状态机生命周期事件? 在现代前端开发中,状态管理(State Management)是构建复杂应
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考