实现带3次重试的动态JS脚本加载功能时appendChild报错如何解决
问题原因
原实现存在三个核心问题,导致异常无法捕获、执行逻辑不符合预期:
- 执行顺序错位:
document.head.appendChild(element)执行在Promise初始化之前,脚本开始加载时,内部存储的resolve/reject还是初始的空函数,若加载回调触发早于Promise初始化,加载结果会直接丢失。 - 递归链路断裂:外层调用方拿到的永远是第一次函数调用返回的Promise,后续递归调用生成的新Promise没有和外层Promise关联;同时递归时传入的共享状态对象会被每次新建的Promise覆盖
resolve/reject回调,最外层的Promise始终无法正常决议,递归内部抛出的DOM操作异常、加载错误都不会进入外层Promise的捕获链。 - DOM操作无防护:
removeChild没有提前判断元素是否真的存在于DOM树中,极端场景下该操作本身就会抛出未捕获异常;重试时新创建的script元素append操作如果出现异常,也因为处于异步回调中没有被Promise包裹,无法被外层捕获。
修复方案
用Promise链透传重试结果的写法替代共享状态对象+递归的逻辑,所有DOM操作都包裹在Promise执行上下文里,所有异常统一归到Promise的reject逻辑,重试计数独立维护,从根源避免链路断裂问题。
async function getScript(url: string, maxRetry = 3): Promise<boolean> { let retryCount = 0; const load = (): Promise<boolean> => { return new Promise((resolve, reject) => { const element = window.document.createElement('script'); element.src = url; element.type = 'text/javascript'; element.async = true; element.onload = () => { resolve(true); }; element.onerror = () => { // 安全移除加载失败的脚本元素 if (element.parentNode) { element.parentNode.removeChild(element); } // 未达最大重试次数则递归重试,结果直接透传到当前Promise if (retryCount < maxRetry) { retryCount++; console.log(`retry load script, current retry count: ${retryCount}`); load().then(resolve, reject); return; } reject(new Error(`Dynamic Script Error: ${url}`)); }; // append操作加异常防护,错误直接进入reject逻辑 try { document.head.appendChild(element); } catch (err) { reject(err); } }); }; return load(); }
修复点说明
- 去掉了共享状态对象,每次加载都返回独立的Promise,重试时直接把新Promise的结果透传给外层,整个调用链完全连通,加载成功、重试成功、加载失败、DOM操作异常所有场景的结果都能被外层正常捕获。
- DOM操作全加防护:移除脚本前先判断父节点存在,append操作包裹try/catch,从根源避免DOM操作抛出未捕获异常。
- 重试计数独立维护,不会因为递归传参混乱导致计数错误,最大重试次数支持自定义。
- 调用方式和原逻辑完全兼容,直接用
await getScript('target.js')即可,成功返回true,超过最大重试次数或出现操作异常时会正常抛出错误,可直接通过try/catch捕获。
内容的提问来源于stack exchange,提问作者duocai
相关产品推荐
相关产品推荐

