JS变量加载延迟致添加菜单失效的问题排查
问题描述
开发了一款具备“添加至我的图书馆”功能的应用,点击“+”按钮可通过JS打开添加菜单,但该功能时常无法正常工作。报错信息如下:
main.js:2 Uncaught TypeError: Cannot read properties of null (reading 'value') at main.js:2:55
相关代码
JS代码
if (window.location.pathname === '/dashboard/') { let booksLen = document.getElementById("booksLen").value; // takes number of positions for (let comp = 1; comp < booksLen; comp++) { let bookAddButton = document.getElementById(`add-btn-${comp}`) let bookAddComp = document.getElementById(`add-comp-${comp}`) bookAddButton.addEventListener('click', () => { if (bookAddComp.classList.contains('d-none') === false) { bookAddComp.classList.toggle('d-none') } else { // disable all previously opened submit forms and error msg for (let compDisable = 1; compDisable < 5; compDisable++) { let bookAddCompDisable = document.getElementById(`add-comp-${compDisable}`) let errMsgDisable = document.getElementById(`err-msg-${comp}`) if (bookAddCompDisable.classList.contains('d-none') === false) { bookAddCompDisable.classList.toggle('d-none') } if (errMsgDisable.classList.contains('d-none') === false) { errMsgDisable.classList.toggle(('d-none')) } } // open only one submit form at a time bookAddComp.classList.toggle('d-none') } }) let submitButton = document.getElementById(`submit-button-${comp}`) // show error msg submitButton.addEventListener('click', e => { let bookInput = document.getElementById(`input-${comp}`).value let errMsg = document.getElementById(`err-msg-${comp}`) if (bookInput === "") { errMsg.classList.remove('d-none') e.preventDefault() } }) } }
模板变量传递代码
<input type="hidden" id="booksLen" name="booksLen" value="{{ books_len }}">
base.html脚本引入
<script src="{% static './js/main.js' %}" async defer></script>
Django视图代码
class BooksAvailability(View): # displays books related to libraries filtered for proper user. Only records desired by user def get(self, request): user = request.user libraries = models.Libraries.objects.all() books = models.Books.objects.filter(user=user.id) status = models.BooksLibraries.objects.all() books_len = len(books) + 1 # passes variable for js purpose. Book add to my library feature return render(request, 'library/dashboard.html', context={'libraries': libraries, 'status': status, 'books': books, 'books_len':books_len})
问题原因及解决方案
核心原因
- DOM加载时机不匹配:虽然脚本加了
async defer,但defer的执行时机是DOM加载完成后,但如果页面渲染慢、模板中booksLen元素位置靠后,或者Django模板渲染有延迟,脚本执行时该元素还未被渲染,就会导致document.getElementById("booksLen")返回null,触发报错。 - 无存在性校验:代码直接获取
booksLen的value,没有做元素存在性判断,一旦元素缺失,后续所有按钮绑定逻辑都会中断。 - 直接统计元素无效的原因:你尝试统计元素数量无效,是因为统计时机不对——DOM未加载完成时,找不到目标元素,统计结果为0,循环根本不会执行。
解决方案
1. 确保DOM完全加载后执行JS
把代码包裹在DOMContentLoaded事件监听中,同时给每个DOM元素添加存在性校验,避免单个元素缺失导致整个逻辑崩溃:
document.addEventListener('DOMContentLoaded', function() { if (window.location.pathname === '/dashboard/') { // 先校验核心元素是否存在 const booksLenEl = document.getElementById("booksLen"); if (!booksLenEl) return; let booksLen = parseInt(booksLenEl.value, 10); // 校验数值有效性 if (isNaN(booksLen) || booksLen <= 1) return; for (let comp = 1; comp < booksLen; comp++) { let bookAddButton = document.getElementById(`add-btn-${comp}`); let bookAddComp = document.getElementById(`add-comp-${comp}`); // 单个元素缺失则跳过当前循环 if (!bookAddButton || !bookAddComp) continue; bookAddButton.addEventListener('click', () => { // 原有点击逻辑 if (bookAddComp.classList.contains('d-none') === false) { bookAddComp.classList.toggle('d-none') } else { for (let compDisable = 1; compDisable < 5; compDisable++) { let bookAddCompDisable = document.getElementById(`add-comp-${compDisable}`) let errMsgDisable = document.getElementById(`err-msg-${comp}`) if (bookAddCompDisable && !bookAddCompDisable.classList.contains('d-none')) { bookAddCompDisable.classList.toggle('d-none') } if (errMsgDisable && !errMsgDisable.classList.contains('d-none')) { errMsgDisable.classList.toggle('d-none') } } bookAddComp.classList.toggle('d-none') } }); let submitButton = document.getElementById(`submit-button-${comp}`); if (!submitButton) continue; submitButton.addEventListener('click', e => { let bookInput = document.getElementById(`input-${comp}`); let errMsg = document.getElementById(`err-msg-${comp}`); if (!bookInput || !errMsg) return; if (bookInput.value === "") { errMsg.classList.remove('d-none') e.preventDefault() } }) } } });
2. 优化模板渲染逻辑
把booksLen的隐藏输入框放在页面靠前位置,确保脚本执行前该元素已被渲染;同时在Django视图中保证books_len始终是有效数值(比如用户无书籍时,books_len为1,此时循环不会执行,避免无效操作)。
3. 替代方案:不依赖hidden字段,直接统计按钮数量
摆脱对booksLen变量的依赖,直接通过选择器获取所有添加按钮,更可靠:
document.addEventListener('DOMContentLoaded', function() { if (window.location.pathname === '/dashboard/') { // 直接获取所有以add-btn开头的按钮 const bookAddButtons = document.querySelectorAll('[id^="add-btn-"]'); bookAddButtons.forEach(button => { // 从按钮ID中提取comp数值 const comp = button.id.split('-')[2]; const bookAddComp = document.getElementById(`add-comp-${comp}`); if (!bookAddComp) return; button.addEventListener('click', () => { // 原有点击逻辑 if (!bookAddComp.classList.contains('d-none')) { bookAddComp.classList.toggle('d-none') } else { for (let compDisable = 1; compDisable < 5; compDisable++) { let bookAddCompDisable = document.getElementById(`add-comp-${compDisable}`) let errMsgDisable = document.getElementById(`err-msg-${comp}`) if (bookAddCompDisable && !bookAddCompDisable.classList.contains('d-none')) { bookAddCompDisable.classList.toggle('d-none') } if (errMsgDisable && !errMsgDisable.classList.contains('d-none')) { errMsgDisable.classList.toggle('d-none') } } bookAddComp.classList.toggle('d-none') } }); const submitButton = document.getElementById(`submit-button-${comp}`); if (!submitButton) return; submitButton.addEventListener('click', e => { const bookInput = document.getElementById(`input-${comp}`); const errMsg = document.getElementById(`err-msg-${comp}`); if (!bookInput || !errMsg) return; if (bookInput.value === "") { errMsg.classList.remove('d-none') e.preventDefault() } }) }); } });
内容的提问来源于stack exchange,提问作者Jumper
相关产品推荐
相关产品推荐

