动态加载JS文件:如何确认代码已执行并可用?
动态加载JS的执行确认方案及完整流程解析
咱先拆解你遇到的问题:你用Promise做了依赖链加载,脚本也按顺序插了HEAD,但onload和MutationObserver都没法精准判断第三方JS是否真的执行完毕——这俩工具的触发时机本来就和代码执行不是一回事,下面给你讲清楚原因、靠谱的解决方案,以及浏览器处理动态脚本的完整流程。
一、为啥onload和MutationObserver不好使?
onload事件:它只负责告诉你“脚本文件下载完并插入DOM了”,但浏览器执行JS的时机是要看主线程档期的。如果当时主线程在忙别的(比如渲染页面、跑其他JS),下载好的脚本会进执行队列排队,这时候onload触发了,但代码还没开始执行,自然找不到全局变量。- MutationObserver:它只能监听DOM节点的增删改,也就是知道“脚本标签被插进HEAD了”,但完全管不着脚本里的代码什么时候执行——标签插入和代码执行是浏览器处理流程里两个完全独立的步骤。
二、针对第三方JS的可靠检测方案
既然不能改第三方代码,最稳妥的办法就是轮询检测它暴露的全局特征(比如jQuery的$、Bootstrap的$.fn.modal),结合Promise封装成可复用的工具,确保依赖真正可用后再加载下一个脚本。
1. 通用的条件等待函数
先写一个能等任意条件满足的Promise工具,比单纯等变量更灵活:
function waitForCondition(condition, timeout = 10000) { return new Promise((resolve, reject) => { // 每隔50ms检查一次条件 const checkInterval = setInterval(() => { if (condition()) { clearInterval(checkInterval); resolve(); } }, 50); // 超时兜底,避免无限等待 setTimeout(() => { clearInterval(checkInterval); reject(new Error('等待条件满足超时,请检查脚本是否加载成功')); }, timeout); }); }
2. 结合你的依赖加载流程使用
把这个工具和你的loadScript结合,就能保证依赖真正可用后再走下一步:
// 基础的脚本加载Promise函数 function loadScript(src) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.onload = () => resolve(script); script.onerror = (err) => reject(new Error(`加载脚本 ${src} 失败: ${err.message}`)); document.head.appendChild(script); }); } // 按依赖链加载:jQuery → Bootstrap → 你的代码 async function loadDependencyChain() { try { // 先加载jQuery,然后等它的全局变量出现 await loadScript('jquery.min.js'); await waitForCondition(() => typeof window.jQuery !== 'undefined'); // 再加载Bootstrap,等它的特征(比如modal方法)出现 await loadScript('bootstrap.min.js'); await waitForCondition(() => { return typeof window.jQuery !== 'undefined' && typeof jQuery.fn.modal !== 'undefined'; }); // 最后加载你的代码,这时候依赖都稳了 await loadScript('your-script.js'); console.log('所有脚本加载并执行完成'); } catch (err) { console.error('加载流程出错:', err); } } // 启动加载 loadDependencyChain();
如果只是等单个全局变量,也可以封装个简化版的waitForGlobalVar,本质和上面的函数逻辑一致。
三、浏览器处理动态脚本的完整流程
搞懂这个流程,你就明白为啥之前的方法会踩坑了:
- 发起下载:当你把
<script>标签插进HEAD,浏览器会异步发起HTTP请求下载脚本(除非你加了async=false,那会变成同步加载阻塞主线程)。 - 进入执行队列:脚本下载完成后,不会立刻执行——它会被放到主线程的任务队列里排队。如果此时主线程正在执行其他任务(比如渲染页面、处理用户交互、跑其他JS),它就得等前面的任务做完。
- 执行脚本:轮到这个脚本时,浏览器会同步执行里面的所有代码:变量声明、函数定义、立即执行的逻辑都会在这一步完成,执行完后才会把全局变量挂载到
window上。 - 触发onload:按照规范,
onload应该是在脚本执行完成后才触发,但实际中可能因为浏览器的任务调度优先级(比如高优先级的渲染任务插队),导致onload和代码执行之间有极短的间隙——这就是你调试时看到onload触发了但变量还没出现的原因。
补充:像jQuery、Bootstrap这类第三方库都是同步执行的,没有异步逻辑,所以只要它们的全局变量出现了,就代表整个脚本已经执行完毕,所有功能都可用。
四、额外优化小技巧
- 缓存检测结果:如果同一个脚本可能被多次加载,可以把检测通过的全局变量缓存起来,避免重复轮询。
- 调整轮询间隔:50ms的间隔是平衡性能和响应速度的折中值,如果对响应速度要求高,可以改成20ms,不过一般50ms足够了。
- 错误处理要到位:一定要加try/catch,不然某个脚本加载失败会导致整个流程挂死。
内容的提问来源于stack exchange,提问作者SBG
相关产品推荐
相关产品推荐

