先记住这个答案
当 HTML 文档开始下载时,document.readyState 为 loading;HTML 解析器完成解析、DOM 可安全访问,但图片、样式表等子资源仍在加载时变为 interactive,随后 DOMContentLoaded 触发;当所有子资源加载完成后变为 complete,然后 load 事件触发。通过监听 readystatechange 事件,可以在状态切换时执行对应初始化逻辑,作为 DOMContentLoaded 或 load 事件的替代方案。
loading:HTML 解析未完,DOM 不可靠。interactive:HTML 解析完,子资源未齐。complete:子资源均完,load将触发。
状态切换的精确时机
document.readyState 由 HTML 标准规定,反映 HTML 解析器和资源加载的进度。当浏览器开始请求文档时,状态为 loading,此时 document.open() 之后的 HTML 字节流还在被解析,DOM 树尚未闭合,脚本若在此时执行会因节点不完整而可能找不到目标元素,也会阻塞解析进程。一旦解析器遇到 </html> 并完成 DOM 构造,状态立即变为 interactive,表示解析完成但外部资源(如图片、样式表、iframe、带 defer 或 module 的脚本)仍在下载。
从 interactive 到 complete 的转变取决于事件循环中剩余的下载任务。规范要求,当所有子资源(包括延迟脚本和模块脚本)执行完毕,且文档没有尚未完成的加载任务时,状态设为 complete,随后立刻派发 load 事件。需要区分的是:DOMContentLoaded 是在进入 interactive 后、延迟脚本执行完时触发,而 readystatechange 到 interactive 的监听器实际上早于 DOMContentLoaded,因此常被用来在解析完成后、DOMContentLoaded 之前插入非延迟初始化。
在交互状态插入可操作 DOM
假设页面有一个统计脚本,需在 DOM 完整但图片尚未加载时快速读取首屏区域的 DOM 属性。图片通常经过 CDN,可能延迟数秒。若等待 window.onload 则首屏指标被拖慢,若在解析期间执行则节点不存在。工程方案是监听 readystatechange,当状态变为 interactive 时执行测量代码,此时 HTML 已完整解析,所有标签均可访问,且避免等待外部资源阻塞。关键约束是此时样式表可能尚未完全就绪,若需要读取 getComputedStyle 等几何信息,必须确保引用的 CSS 已加载,否则可能得到中间值。
为正确处理,可先检查 document.styleSheets 中关键样式表是否完成加载,或直接依赖标准场景:通常内联脚本在 interactive 前执行,而外部样式表会阻塞脚本执行,故当 readystatechange 到达 interactive 时,执行过的主文档脚本已确保其之前的 CSS 已应用。此例中,测量逻辑放在 readystatechange 回调内,而非 DOMContentLoaded,因为后者在延迟脚本执行后才触发,若测量要求尽量早,则前者更优。最终,代码在 HTML 解析完成瞬间运行,取得稳定 DOM 状态,并将结果上报,图片加载完成与否不影响该数据。
容易失效的假设与兜底
错误假设之一是 interactive 意味着所有脚本(包括 defer 和 module)已经执行。实际上,readystatechange 到 interactive 的时刻出现在文档解析完成后,但 defer 和 module 脚本会在 interactive 之后、DOMContentLoaded 之前按顺序执行,因此此时它们可能尚未运行。同步普通脚本(无 defer/async)会阻塞解析并在解析过程中执行,所以 interactive 时它们已经完成。真正的问题是,如果页面中存在动态插入的脚本或模块,其执行时机可能晚于 interactive,此时 DOM 虽可访问但不代表所有脚本副作用已发生,因此依赖某些库注册的全局对象时可能仍未定义。
另一失效点是,若页面被取消加载(如用户停止导航),readyState 可能停在 interactive 而不进入 complete,这时依赖 complete 做清理工作的逻辑永远不会触发。处理方法是结合 visibilitychange 或 pagehide 事件来兜底。此外,若文档包含同步的 iframe,其加载会影响 complete 的判定,但 interactive 不受影响,因此测量首屏时注意与 iframe 解耦。为可靠,可在 readystatechange 回调中判断 document.readyState 并分支,同时在 window 再监听 load 做最终兜底,代价是少量重复代码,但能覆盖早期状态变化时序不确定的边缘情况。
容易答错的地方
- 认为 complete 等于 load 触发
- 正确顺序是
readyState变为complete后,load事件才触发。它们之间几乎同步,但并非同时,load事件稍晚。若只监听readystatechange,内部判断complete时可用于替代load,但若要精确等待load事件,应直接监听load。 - 在 loading 中操作动态插入脚本
- 有些开发者误以为在“loading”阶段动态插入脚本会像解析时遇到的
<script>一样同步执行,实际上通过 DOM 方法插入的脚本默认是异步的,执行点不确定;若使用document.write则可能重写文档。更可靠的做法是把初始化代码放在readystatechange的interactive分支或DOMContentLoaded中。
面试官还会怎么问?
readystatechange 到 interactive 时,defer 脚本执行完了吗?
尚未。defer 和 module 脚本会在进入 interactive 后、触发 DOMContentLoaded 之前按顺序执行,所以当 readystatechange 到 interactive 时,它们还未执行。
为什么用 readystatechange 而不是直接监听 DOMContentLoaded?
readystatechange 到 interactive 早于 DOMContentLoaded,因此在需要更早介入的场景可考虑使用;但日常场景推荐直接监听 DOMContentLoaded,语义更清晰。
readyState 可能从 interactive 回到 loading 吗?
不会,readyState 的变化是单向的:从 loading 到 interactive 再到 complete。重新加载新文档会创建新的 Document 对象,旧对象状态不再变化。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。