跳到主要内容

页面加载事件

页面生命周期有哪些关键事件?​

事件触发对象时机
DOMContentLoadeddocumentHTML 解析完、DOM 树建成, 图片等外部资源可能未加载
loadwindow页面与所有外部资源 (图片、样式) 都加载完毕
beforeunloadwindow用户即将离开, 还能询问是否确认
unloadwindow用户已经离开, 只能做不阻塞的收尾
  • 用途: 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 也能跟踪状态变化):

  1. readyState:loading;
  2. readyState:interactive;
  3. DOMContentLoaded;
  4. 各资源 load (iframe onload、img onload);
  5. readyState:complete;
  6. 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 标志也能做这种"离开页面后"的通用请求;