本文详解如何可靠检测多个动态加载的 JS 文件是否全部完成加载与执行,重点解决按序依赖场景下的时序控制问题,涵盖 onload 属性局限、defer 的适用边界、现代 Promise 链式加载方案及错误容错机制。
本文详解如何可靠检测多个动态加载的 js 文件是否全部完成加载与执行,重点解决按序依赖场景下的时序控制问题,涵盖 `onload` 属性局限、`defer` 的适用边界、现代 promise 链式加载方案及错误容错机制。
在前端开发中,动态懒加载多个 JavaScript 文件(如插件、第三方 SDK 或模块化功能脚本)是常见的性能优化手段。但当这些脚本存在执行依赖关系(例如 file2.js 依赖 file1.js 中定义的全局对象,file3.js 又依赖前两者)时,仅靠 <script onload="allJsLoaded()"> 绑定到<strong>最后一个 script 标签是<strong>不可靠的——原因如下:</script>
- ✅ onload 事件仅表示该 <script> 标签的资源<strong>下载并执行完毕; </script>
- ❌ 但它不保证前序脚本已执行完成:浏览器对无 defer/async 的普通 <script> 默认采用<strong>阻塞解析、顺序执行策略,但若脚本被动态插入(document.createElement('script')),其加载行为会退化为异步(尤其在现代浏览器中),导致执行顺序不可控; </script>
- ❌ 更关键的是:onload 是单脚本粒度事件,无法感知整个依赖链的状态,一旦中间某脚本因网络失败、语法错误或执行异常而中断,file3.js.onload 仍会触发,造成 allJsLoaded() 误判。
正确方案:使用 Promise 链式加载(推荐)
最健壮、可读性强且支持错误捕获的方式,是将每个脚本加载封装为 Promise,并按需串行或并行执行:
function loadScript(src) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = src;
script.type = 'text/javascript';
script.onload = () => resolve(src);
script.onerror = () => reject(new Error(`Failed to load ${src}`));
document.head.appendChild(script);
});
}
// 按序加载(确保 file1 → file2 → file3 严格执行)
Promise.all([
loadScript('./js/file1.js'),
loadScript('./js/file2.js'),
loadScript('./js/file3.js')
])
.then(() => {
console.log('✅ All scripts loaded and executed in order');
allJsLoaded(); // 安全调用
})
.catch(err => {
console.error('❌ Script loading failed:', err);
// 可选:降级处理、上报监控、提示用户
});
✅ 优势:显式声明依赖、自动等待全部 resolve、统一错误处理、兼容所有现代浏览器(包括动态插入场景)。
替代方案对比与适用场景
| 方案 | 触发时机 | 是否保证执行顺序 | 是否感知脚本错误 | 推荐场景 |
|---|---|---|---|---|
| script.onload on last tag | 仅最后脚本加载完 | ❌ 否(动态插入下顺序不保) | ✅ 是(需手动加 onerror) | 简单无依赖的单脚本回调 |
| <script defer> + window.onload</script> | HTML 解析完 + 所有 defer 脚本执行完 | ✅ 是(浏览器保障顺序) | ❌ 否(错误静默,不触发 onload) | 静态 HTML 中预置脚本,且可接受错误无感知 |
| DOMContentLoaded | DOM 构建完成(不含 JS 执行) | ❌ 否(JS 可能未执行) | ❌ 否 | 仅需操作 DOM,不依赖外部脚本逻辑 |
| window.load | 所有资源(图片/CSS/JS)加载完毕 | ❌ 否(JS 执行仍可能未完成) | ❌ 否 | 需等待图片等媒体资源,不适用于 JS 加载状态判断 |
⚠️ 特别注意:<script defer> <strong>仅对静态 HTML 中声明的脚本生效,对 document.createElement('script') 动态创建的脚本无效;同时 defer 不支持 onload 回调属性(W3C 规范限制),因此原问题中 <script defer onload="..."> 写法实际会被忽略。</script>
进阶:支持超时与重试的生产级封装
对于关键业务脚本,建议增强鲁棒性:
function loadScriptWithRetry(src, { timeout = 10000, maxRetries = 2 } = {}) {
return new Promise((resolve, reject) => {
const attempt = (retryCount = 0) => {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const script = document.createElement('script');
script.src = src;
script.type = 'text/javascript';
script.onload = () => {
clearTimeout(timer);
resolve(src);
};
script.onerror = () => {
clearTimeout(timer);
if (retryCount {
script.remove();
reject(new Error(`${src} loading timed out`));
});
document.head.appendChild(script);
};
attempt();
});
}
总结
- ❌ 不要依赖 onload 绑定到最后一个动态 script 标签来判断“全部加载完成”——它既不保序也不容错;
- ✅ 优先使用 Promise 封装 + Promise.all() 实现可预测、可调试、可监控的加载流程;
- ? 避免混用 defer 与动态插入,defer 仅适用于 HTML 静态声明;
- ? 若必须用事件监听,请明确区分:DOMContentLoaded(DOM 就绪)、window.onload(资源就绪)、script.onload(单文件就绪),三者语义与时机截然不同。
通过结构化加载控制,你不仅能确保 allJsLoaded() 在真正安全的时机执行,更能为后续功能提供稳定可靠的运行环境。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











