如何在页面加载时控制多脚本执行并确保DOM加载完成后脚本正确运行?
嘿,这个问题我之前刚踩过同款坑!你说的没错,jQuery的$(function(){...})确实够贴心,不管什么时候调用,DOM就绪了就直接跑回调,没就绪就乖乖等,但原生JS的DOMContentLoaded是真的只触发一次——要是你的脚本在事件触发之后才加载完成(比如你index.js里有await请求拖慢了执行时机),那绑定的监听函数就彻底没机会跑了。
给你分享几个我日常开发里用的靠谱方案:
1. 封装一个类似jQuery ready的工具函数(最推荐)
自己写个几行的小工具,完美兼容“DOM已就绪”和“未就绪”两种情况,用起来和jQuery的ready几乎一模一样:
先在所有脚本都能访问到的地方(比如公共的menu.js最开头,或者单独的公共工具脚本)定义这个函数:
function onDomReady(callback) { // 先检查DOM当前状态:interactive表示DOM已经解析完成,complete是所有资源都加载完毕 if (document.readyState === 'interactive' || document.readyState === 'complete') { // 已经就绪,直接执行回调 callback(); } else { // 还没准备好,监听DOMContentLoaded事件 document.addEventListener('DOMContentLoaded', callback); } }
然后不管是menu.js还是index.js,都用这个函数包起DOM相关逻辑:
menu.js里:
onDomReady(() => { document.querySelector(".navbar-menu").addEventListener('click', () => { // 你的菜单点击逻辑 }); });
index.js里(先拿数据再等DOM,逻辑更清晰):
// 先异步拿数据,这部分不依赖DOM const { httpConstants } = (await axios.get('/some-data')).data; // 再用工具函数处理DOM相关逻辑 onDomReady(() => { // 这里放页面特定的DOM操作逻辑 });
这个方法的好处是复用性拉满,不管脚本加载早或晚,甚至是异步加载的,都能正确触发回调。
2. 直接检查readyState后执行(适合简单场景)
要是不想封装函数,也可以在每个脚本的DOM逻辑前直接判断状态:
比如index.js里可以这么写:
const { httpConstants } = (await axios.get('/some-data')).data; // 直接判断DOM是否就绪 if (document.readyState === 'interactive' || document.readyState === 'complete') { // 已经就绪,直接执行DOM逻辑 // ...你的页面专属逻辑 } else { // 没就绪,等DOMContentLoaded事件 document.addEventListener('DOMContentLoaded', () => { // ...你的页面专属逻辑 }); }
不过这种写法会重复代码,多个脚本都这么写就很繁琐,还是封装成函数更省心。
3. 利用ES模块的默认行为(针对你的index.js)
你要知道,浏览器加载type="module"的脚本时,默认是延迟执行的——也就是会等DOM解析完成后才执行模块的顶层代码。但你index.js里的await axios.get(...)会让模块执行暂停,直到请求完成,这时候DOMContentLoaded可能已经触发完了,所以你后来加的addEventListener就监听不到了。
如果你的异步请求不需要在DOM就绪前做,其实可以把await放到DOM就绪的回调里:
onDomReady(async () => { // 等DOM就绪了再拿数据,避免错过事件 const { httpConstants } = (await axios.get('/some-data')).data; // ...你的页面逻辑 });
这样就不会因为异步请求拖慢,导致错过DOMContentLoaded事件了。
最后再给你提个醒:绝对不要手动触发DOMContentLoaded事件!你之前试的那个方法会触发递归错误,只要触发这个事件,所有监听它的回调都会执行,要是某个回调里又不小心触发了(或者有其他逻辑间接触发),直接就无限循环炸锅😂。




