You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

addEventListener设为once仍多次触发,致应用动画异常及事件绑定问题

问题:多次点击添加提示按钮后,关闭按钮事件重复触发且无法正确移除

点击添加提示按钮(addTodoBtnLm)时,会为提示框的关闭按钮(closePromptBtn)绑定点击事件监听器。单次操作时关闭按钮的监听器可正常移除,但多次点击添加提示按钮后,监听器无法被正确移除;即使设置{once: true},点击关闭按钮时事件仍会多次触发(控制台多次输出'wrapper'),导致应用动画异常。

代码示例

const addTodoBtnLm = document.getElementById('todo-app-intro__add-btn');
const refreshQuoteBtn = document.getElementById('quote__btn');
const timsIntroBtns = {};

function checkActiveBtn(btnLm) {
  if (btnLm.getAttribute('aria-expanded') === 'false') {
    btnLm.classList.add('btn--active');
  } 
  else {  
    btnLm.classList.remove('btn--active');
  }
};

function showPrompt(hideTmId, promptLm, btnLm) {
  clearTimeout(hideTmId);
  promptLm.removeAttribute('hidden');
  btnLm.setAttribute('aria-expanded', true);
  setTimeout(() => {
    promptLm.classList.add('todo-app-prompt--active');
  }, 0);
}

function setHideTim(timeoutId, promptLm) {
  timsIntroBtns[timeoutId] = setTimeout(() => {
    promptLm.setAttribute('hidden', '');
  }, 1500);
}

function hidePrompt(promptLm, btnLm, closeBtnLm, wrappingFunction) {
  closeBtnLm.removeEventListener('click', wrappingFunction);
  btnLm.setAttribute('aria-expanded', false);
  promptLm.classList.remove('todo-app-prompt--active');
}

function addTodoPrompt() {
  const todoAppPromptLm = document.getElementById('todo-app-prompt');
  const closePromptBtn = document.getElementById('todo-app-prompt__cancel-btn');

  function wrappingFunction() {

/*多次点击添加按钮后,点击关闭按钮时控制台会多次输出'wrapper',次数与添加按钮点击次数一致*/
    console.log('wrapper');

    checkActiveBtn(addTodoBtnLm);
    setHideTim('promptHideTmId2', todoAppPromptLm)
    hidePrompt(todoAppPromptLm, addTodoBtnLm, closePromptBtn, wrappingFunction);
  }

  checkActiveBtn(addTodoBtnLm);

  if (todoAppPromptLm.matches('.todo-app-prompt--active')) {
    setHideTim('promptHideTmId', todoAppPromptLm)

//此处应移除监听器,但未生效
    hidePrompt(todoAppPromptLm, addTodoBtnLm, closePromptBtn, wrappingFunction);
  } 
  else {
    showPrompt(timsIntroBtns.promptHideTmId, todoAppPromptLm, addTodoBtnLm);
    clearTimeout(timsIntroBtns.promptHideTmId2);
    closePromptBtn.addEventListener('click', wrappingFunction);
  }
}

addTodoBtnLm.addEventListener('click', () => {
  addTodoPrompt();
});

已尝试方案

  • 在检测提示框已打开的if语句中调用removeEventListener,但未生效;
  • 为closePromptBtn的addEventListener设置{once: true},虽可移除removeEventListener代码,但事件仍会累加触发。

问题根源

每次调用addTodoPrompt时,都会创建一个全新的wrappingFunction函数实例。removeEventListener需要与绑定事件时完全相同的函数引用才能生效,所以用新创建的函数去移除旧的监听器根本找不到,导致旧的监听器一直残留。多次点击添加按钮后,就会绑定多个独立的wrappingFunction,点击关闭按钮时所有这些函数都会被触发。

解决方案

把wrappingFunction的引用统一保存,确保绑定和移除时用的是同一个函数实例。以下是修改后的代码:

const addTodoBtnLm = document.getElementById('todo-app-intro__add-btn');
const refreshQuoteBtn = document.getElementById('quote__btn');
const timsIntroBtns = {};
// 全局保存当前的关闭按钮监听器引用
let currentCloseListener = null;

function checkActiveBtn(btnLm) {
  if (btnLm.getAttribute('aria-expanded') === 'false') {
    btnLm.classList.add('btn--active');
  } 
  else {  
    btnLm.classList.remove('btn--active');
  }
};

function showPrompt(hideTmId, promptLm, btnLm) {
  clearTimeout(hideTmId);
  promptLm.removeAttribute('hidden');
  btnLm.setAttribute('aria-expanded', true);
  setTimeout(() => {
    promptLm.classList.add('todo-app-prompt--active');
  }, 0);
}

function setHideTim(timeoutId, promptLm) {
  timsIntroBtns[timeoutId] = setTimeout(() => {
    promptLm.setAttribute('hidden', '');
  }, 1500);
}

function hidePrompt(promptLm, btnLm, closeBtnLm) {
  // 使用统一引用移除监听器
  if (currentCloseListener) {
    closeBtnLm.removeEventListener('click', currentCloseListener);
    currentCloseListener = null;
  }
  btnLm.setAttribute('aria-expanded', false);
  promptLm.classList.remove('todo-app-prompt--active');
}

function addTodoPrompt() {
  const todoAppPromptLm = document.getElementById('todo-app-prompt');
  const closePromptBtn = document.getElementById('todo-app-prompt__cancel-btn');

  // 定义监听器函数
  function wrappingFunction() {
    console.log('wrapper');
    checkActiveBtn(addTodoBtnLm);
    setHideTim('promptHideTmId2', todoAppPromptLm)
    hidePrompt(todoAppPromptLm, addTodoBtnLm, closePromptBtn);
  }

  checkActiveBtn(addTodoBtnLm);

  if (todoAppPromptLm.matches('.todo-app-prompt--active')) {
    setHideTim('promptHideTmId', todoAppPromptLm)
    hidePrompt(todoAppPromptLm, addTodoBtnLm, closePromptBtn);
  } 
  else {
    showPrompt(timsIntroBtns.promptHideTmId, todoAppPromptLm, addTodoBtnLm);
    clearTimeout(timsIntroBtns.promptHideTmId2);
    // 先移除旧监听器,再绑定新的
    if (currentCloseListener) {
      closePromptBtn.removeEventListener('click', currentCloseListener);
    }
    currentCloseListener = wrappingFunction;
    closePromptBtn.addEventListener('click', currentCloseListener);
  }
}

addTodoBtnLm.addEventListener('click', () => {
  addTodoPrompt();
});

关键修改点

  • 新增currentCloseListener变量统一保存监听器引用;
  • 在hidePrompt中使用该引用移除监听器,避免无效移除;
  • 绑定新监听器前先移除旧的,防止重复绑定;
  • 移除hidePrompt的wrappingFunction参数,直接使用全局保存的引用。

这样无论点击多少次添加按钮,关闭按钮始终只会绑定一个有效监听器,点击关闭时只会触发一次,动画也能正常执行。

内容的提问来源于stack exchange,提问作者Angel

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 02:15:16