页面加载事件
页面生命周期有哪些关键事件?
| 事件 | 触发对象 | 时机 |
|---|---|---|
DOMContentLoaded | document | HTML 解析完、DOM 树建成, 图片等外部资源可能未加载 |
load | window | 页面与所有外部资源 (图片、样式) 都加载完毕 |
beforeunload | window | 用户即将离开, 还能询问是否确认 |
unload | window | 用户已经离开, 只能做不阻塞的收尾 |
- 用途:
DOMContentLoaded里查节点、初始化界面;load里读图片尺寸、读依赖样式的计算结果; - 写法:
DOMContentLoaded必须用addEventListener监听,document.onDOMContentLoaded不存在;
document.addEventListener("DOMContentLoaded", ready); // 不能用 document.onDOMContentLoaded
DOMContentLoaded 何时触发, 谁会让它推迟?
- 前提: 解析到
<script>必须先执行脚本, 脚本可能改 DOM 甚至document.write, 所以事件要等所有脚本执行完; - 例外一: 带
async的脚本不阻塞该事件, 见 脚本加载与文档模式; - 例外二:
document.createElement("script")动态插入的脚本也不阻塞; - 样式陷阱: 外部样式表本身不阻塞, 但样式表之后的脚本要等样式加载完才执行 (脚本可能读
getComputedStyle), 于是事件被间接推迟;
<script>
document.addEventListener("DOMContentLoaded", () => alert("DOM ready!"));
</script>
<script src="lodash.js"></script>
<script>
alert("inline");
</script>
<!-- 顺序: lodash 执行 -> inline 执行 -> DOM ready -->
- 自动填充: 浏览器在
DOMContentLoaded时填充表单, 脚本拖慢它, 登录框的自动填充也会明显延迟; - 资源未就绪: 此时读
<img>尺寸可能得到0x0, 因为图片还没下载完;
readyState 与 load 的先后顺序是什么?
document.readyState:"loading"解析中,"interactive"文档读完 (约等于DOMContentLoaded),"complete"资源也加载完 (约等于window.onload);- 兜底写法: 先判断状态再决定监听还是立刻执行, 否则文档已就绪时处理程序永远不会运行;
function work() {
/* ... */
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", work);
} else {
work(); // DOM 已就绪, 直接执行
}
典型事件流 (readystatechange 也能跟踪状态变化):
readyState:loading;readyState:interactive;DOMContentLoaded;- 各资源
load(iframe onload、img onload); readyState:complete;window.onload;
window.onload: 排在所有其它load处理程序之后; 监听方式用window.onload = fn或addEventListener("load", fn);readystatechange: 出现很早的旧机制, 现在很少用;
如何跟踪单个资源的加载结果?
onload: 加载成功;onerror: 加载失败;- 适用范围: 任何带外部
src的资源 (脚本、图片、样式、iframe); - 失败细节:
onerror拿不到 HTTP 状态码, 只知道失败了;
| 资源 | 特殊之处 |
|---|---|
<script> | 动态插入即开始下载, load 触发时脚本已下载并执行完 |
<img> | 唯一例外, 设置 src 时才开始加载 (不是插入文档时) |
<iframe> | 历史原因, 无论加载成功还是页面不存在, 都只触发 load |
const script = document.createElement("script");
script.src = "my.js";
script.onload = () => alert(_.VERSION); // 可以用脚本里的变量了
script.onerror = () => alert("加载失败");
document.head.append(script);
- 边界: 这些事件只管加载本身, 脚本内部的运行时错误要靠
window.onerror, 见 错误处理;
跨域脚本的报错为什么没有细节?
- 原因: 同源策略 (域名 + 端口 + 协议) 禁止跨源读取, 跨源脚本的报错会被折叠成
Script error.加, 0:0; - 放开方式:
<script>加crossorigin属性, 且远程服务器返回对应 CORS 响应头;
| 写法 | 条件与效果 |
|---|---|
无 crossorigin | 禁止访问, 报错无细节 |
crossorigin="anonymous" | 服务器回 Access-Control-Allow-Origin (* 或本站) 即可, 不发送 Cookie |
crossorigin="use-credentials" | 额外要求 Access-Control-Allow-Credentials: true, 会发送 Cookie |
- 实践: 不涉及凭证时选
"anonymous", 一个响应头即可拿到完整的行号与堆栈; - 范围: 同源策略对所有跨源资源通用, 不只脚本;
beforeunload 与 unload 分别能做什么?
beforeunload: 唯一能阻止离开的时机, 取消事件后浏览器询问用户是否确认;
window.onbeforeunload = () => "有未保存的更改, 确定离开?";
// 等价写法: 返回非空字符串即视为取消
window.addEventListener("beforeunload", (event) => {
event.returnValue = "unsaved"; // 这里 preventDefault() 会被浏览器忽略
});
- 历史: 返回的字符串过去会显示给用户, 因滥用严重, 现代浏览器不再展示它;
unload: 不能延迟跳转、不能询问用户, 只适合发送统计数据;
window.addEventListener("unload", () => {
// POST, 后台发送, 不延迟跳转, 数据上限 64kb, 拿不到响应
navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});
- 同族方案:
fetch的keepalive标志也能做这种"离开页面后"的通用请求;