async脚本执行无序,无法保证依赖脚本先执行;应改用defer、动态加载或es模块。

async脚本执行太快,导致依赖的全局变量还没定义
async脚本一下载完就立刻执行,完全不等其他脚本、也不等DOM解析完成。如果你在utils.js里用var format = () => {...}定义了一个函数,又在另一个带async的main.js里直接调用format(),那大概率报ReferenceError: format is not defined——不是utils.js没加载,是它还没执行完,main.js已经冲过去了。
- async只保证“下载异步”,不保证“执行有序”;多个async脚本谁先下完谁先跑,顺序不可控
- 浏览器对
<script async src="a.js"></script>和<script async src="b.js"></script>不做执行顺序约束,哪怕a写在前面 - 哪怕你把
utils.js放在main.js前面,只要它体积大、网络慢,依然可能被后者抢先执行
为什么不能靠“把依赖脚本放前面”来解决
HTML中书写顺序对async脚本无效。浏览器会并发下载所有带async的脚本,但执行时机只取决于各自下载完成时间。你写成:
<script async src="utils.js"></script><script async src="main.js"></script>
结果可能是main.js先执行、utils.js后执行——尤其当utils.js被CDN缓存命中率低或体积更大时。
- async脚本之间没有执行依赖保障,不能形成“先A后B”的链式逻辑
-
utils.js里用var或let声明的变量,只在自身作用域内有效,不会自动挂到window上(除非显式赋值如window.format = ...) - 即使挂了
window.format,如果main.js执行时utils.js还没执行完,window.format仍是undefined
真正安全的替代方案:改用defer或动态加载
要确保脚本按需、有序、且能访问彼此定义的全局变量,必须放弃async,换更可控的机制。
- 用
defer:适合有依赖关系的业务脚本。例如lodash.js→app.js,两者都加defer,浏览器会严格按HTML顺序执行,且都在DOM解析完成后运行 - 动态插入+回调:在
main.js里用document.createElement('script')手动加载utils.js,监听onload后再执行后续逻辑,可精确控制依赖链 - 模块化方案:改用
type="module",天然支持依赖声明(import),且隐式具备defer行为;但注意路径必须是相对/绝对URL,不能是纯文件名
如果非得保留async,只能靠运行时防御
极少数场景(比如第三方统计SDK)必须用async,而你又需要它提供某个全局变量(如window._gaq),那就不能假设它一定存在,得主动轮询或监听。
- 用
setTimeout+ 递归检查:if (window.format) { format() } else { setTimeout(check, 50) } - 监听自定义事件:让
utils.js执行完后dispatchEvent(new Event('utils:ready')),main.js提前addEventListener('utils:ready', ...) - 避免直接读
window.xxx,改用typeof window.xxx === 'function'做存在性判断再调用
async的本质是“无依赖、不操作DOM、失败不影响主流程”。一旦你开始依赖它导出的东西,就已经违背了它的设计前提——这时候,问题不在怎么防undefined,而在该不该用async。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











