资源重试本身不会导致节点重复挂载,问题在于重试后未清理旧状态或校验当前环境是否已就绪,如失败script未移除、初始化无幂等保护、dom操作未加guard等。

资源重试本身不会直接导致节点重复挂载,但重试逻辑若与 DOM 操作耦合不当,就会触发重复插入、重复初始化、重复事件绑定——最终表现为节点“看起来被挂载了两次”。根源不在重试动作,而在重试后没清理旧状态。
为什么 script 重试后会多出一个同名节点
典型场景是:页面中某个 <script src="widget.js"></script> 加载失败,监听器捕获后动态创建新 <script></script> 并插入 ;而原始 <script></script> 标签仍保留在 DOM 中(浏览器不自动移除失败的 script 标签),后续 widget.js 初始化时又执行了一次 document.body.appendChild(el),结果 el 被 append 两次。
- 失败的
<script></script>元素不会被浏览器自动从 DOM 中移除,e.target依然可访问 - 若重试逻辑只管“插入新 script”,不管“是否已有同功能模块在运行”,就极易重复执行初始化代码
- 第三方脚本常自带自执行逻辑(如 IIFE),无法控制其内部是否幂等,重试等于再跑一遍
如何避免重试引发的 DOM 节点重复
关键不是阻止重试,而是让重试动作和 DOM 状态解耦。所有重试后的操作必须先校验当前环境是否已就绪。
- 给被加载资源打唯一标记:比如在插入前写
document.documentElement.setAttribute('data-widget-loaded', 'true'),重试前先检查该属性是否存在 - 初始化逻辑加 guard:用
if (!window.MyWidgetInstance) { window.MyWidgetInstance = new Widget() },避免多次实例化 - 不要在重试回调里直接操作 DOM,改用事件通知机制:重试成功后 dispatch
CustomEvent('widget:loaded'),由独立监听器负责挂载,且只响应一次 - 对已插入的失败
<script></script>,可在重试前调用e.target.remove()(注意:仅限捕获阶段能拿到 e.target)
onerror + 动态插入 script 的常见坑
很多方案直接在 <script onerror="retry(this)"></script> 里调用 document.head.appendChild(newScript),这看似简单,却埋下三处隐患:
-
this在某些压缩工具或 CSP 环境下可能丢失上下文,推荐用event.target或传入src字符串而非 this - 未清除原 script 的
async/defer属性,新 script 执行时机错乱,可能早于依赖项 - 重试 URL 未加时间戳或版本参数,CDN 缓存返回旧 404 响应,导致无限重试失败循环
- 未限制重试次数,
retryMaps[<code>new URL(src).pathname] 计数漏初始化,或跨页面未清空,造成误判
真正要监控的不是“重试次数”,而是“挂载状态”
你看到的“节点重复”,90% 是因为前端把“资源加载完成”错误等价于“功能已就绪”。而实际中,JS 加载成功 ≠ 执行完毕 ≠ DOM 插入完成 ≠ 事件绑定生效。
- 用
performance.getEntriesByName('widget.js', 'resource')查真实加载耗时,比监听load事件更准 - 初始化完成后手动设置
data-initialized="true"到对应容器节点,后续所有操作都以该属性为判断依据 - 对 iframe 或第三方 widget,用
window.frames[i].postMessage等待对方确认 ready,而不是靠 setTimeout 猜测
节点重复挂载从来不是重试机制的问题,而是状态管理缺失的副产品。重试只是暴露了那个一直没被发现的竞态条件。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











