先记住这个答案
DOMContentLoaded 在文档被完全解析且所有延迟脚本(defer 和 module 脚本)下载并执行后触发,不等待图片、子框架和 async 脚本。load 事件则等待页面所有资源加载完成,包括图片和 iframe。要检测图片加载完成,应监听 load 事件或使用图片自身的 load 事件。
- DOMContentLoaded 只等 HTML 解析与 defer 脚本
- load 等所有子资源(图片、样式、iframe)
- 监听 DOMContentLoaded 会漏掉图片加载
解析与子资源加载的解耦机制
HTML 解析器自上而下处理文档,遇到同步脚本会阻塞解析,遇到 defer 或 module 脚本则延迟到解析结束后执行。DOMContentLoaded 事件在 HTML 解析完成且这些延迟脚本执行完毕时触发。此时外部图片、iframe 以及 async 脚本可能仍在加载或执行,事件不会等待它们。需要注意的是,defer 和 module 脚本会等待样式表加载完成,因此当页面存在这类脚本时,样式表会在事件触发前完成,事件被间接推迟;但若没有这类脚本,普通样式表不会直接阻塞该事件。
load 事件由浏览器在所有资源加载完成后触发。资源包括 <img>、<link> 引用的样式表、<iframe> 内的文档以及媒体文件。若页面存在慢速图片,监听 load 的脚本将明显晚于 DOMContentLoaded 执行。这是两个事件核心区别,也是选择监听对象时的依据。
统计首屏图片加载完成时间
假设页面有 20 张首屏图片,你想要在所有图片可见后执行曝光统计。若使用 document.addEventListener('DOMContentLoaded', ...),回调触发时图片可能只加载了一部分,统计到的尺寸或自然宽度可能失真。在慢网络下,DOMContentLoaded 可能早于最后一张图片加载完成很长时间,具体差异取决于网络状况和资源大小。
正确做法是监听 window.load,但也要考虑图片加载失败和缓存情况。更精细控制可用 Promise.all 结合每张图片的 complete 属性与 load、error 事件。若只需图片真正解码完成,还需检查 decode() 方法。load 事件已能保证资源下载且可显示,但在慢网络下它可能大幅晚于 DOMContentLoaded,具体差异取决于资源数量和网络状况。
失效条件与动态资源陷阱
DOMContentLoaded 不等待 async 脚本、动态插入的图片或通过 JS 创建的 DOM。如果你在 DOMContentLoaded 回调里动态给 <img> 设置 src,该图片的加载会延迟 load 事件(除非 load 已经触发),因此不能认为不影响 load。若需感知该动态图片的加载完成,应使用图片自身的 load 事件。
iframe 本身也是子资源,会延迟 load 事件。若嵌入慢速第三方 iframe,页面 load 可能被拖后几秒。如果需要尽快执行脚本,应避免监听 load;若需统计真实加载完,可接受延迟并设置超时保护,或用 iframe 的 load 事件单独处理。
容易答错的地方
- 在 DOMContentLoaded 中读取图片宽度
- DOMContentLoaded 时图片可能未加载完成,读取
img.naturalWidth可能为 0 或未更新。必须等图片自己的 load 事件或 window load。许多新手在此误判,导致懒加载失效。 - 以为 defer 脚本不等待样式表
- MDN 指出,defer 和 module 脚本会等待样式表加载完成,而 DOMContentLoaded 事件在它们执行后触发,因此脚本是否为 defer 类型直接影响事件时机。若页面只有普通同步脚本(非 defer),这些脚本也会等待样式表,但若没有脚本,样式表不阻塞 DOMContentLoaded。常见误区是笼统宣称「样式表会阻塞 DOMContentLoaded」或「样式表不阻塞任何脚本」,实际需区分脚本类型。
面试官还会怎么问?
如果我的脚本是在 DOMContentLoaded 之后通过 async 方式加载的,如何确保初始化逻辑执行?
检查 document.readyState。若为 'loading',直接监听 DOMContentLoaded;否则立即执行。由于 JS 单线程特性,不存在竞态。这是 MDN 推荐的标准方法。
图片设置了 loading=lazy 是否会影响 load 事件触发?
会。懒加载图片如果不进入视口,就不会触发下载,因此 window.load 可能提前触发而不等待这些图片。只有进入视口的懒加载图片才会被加载,因此 load 事件不保证所有图片加载完。需要结合 IntersectionObserver 自行管理。
DOMContentLoaded 与 document.readyState 的值有什么关系?
DOMContentLoaded 触发前,readyState 会变为 'interactive'(但在执行 defer 脚本之前变为 interactive,defer 执行完后事件触发)。load 触发后变成 'complete'。但本题重点是事件语义,状态值仅是旁证。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。