PAYATHON 2026

JavaScript 待办清单中,为什么创建多个项目后添加任务会重复?

数据小许

任务重复的直接原因是 taskBtn.addEventListener(...) 写在了 projectBtn 的点击回调里。每创建一个项目,同一个任务按钮就会多一个监听器。创建 4 个项目后,点击一次任务按钮会触发 4 个回调,makeTask(...) 也会执行 4 次。

把任务按钮的监听器移到外层,只注册一次,再用变量保存当前选中的项目。

为什么不是单纯的 forEach 问题?

原代码的嵌套关系可以简化为:

projectBtn.addEventListener("click", function () {
  // 每次创建项目时,都会执行这里的代码。

  taskBtn.addEventListener("click", function () {
    // 添加任务。
  });
});

每次执行外层回调时,都会创建一个新的内部函数。即使函数代码相同,addEventListener 仍会把它们作为不同的监听器注册。

forEach 所在的位置也有问题:每创建一个项目,代码都会重新查询全部 .overview 元素,并给每个元素再绑定一次点击事件。较早创建的项目因此会积累多个监听器,点击时,查找项目和更新标题的逻辑也会重复执行。

这两处事件绑定都需要调整。

将事件注册与项目创建分开

下面的示例用事件委托处理动态创建的项目,并用 selectedProject 保存当前项目。

示例沿用已有的 Project、ListItems、makeProject、addProject、findProject、changeProject 和 makeTask。它假设 findProject(id) 返回对应的 Project 对象或空值,且 makeTask 负责把任务渲染到当前项目视图。

用以下代码替换原来的整段事件绑定代码,并确保初始化代码只执行一次:

let selectedProject = null;

// 创建项目:监听器只注册一次。
projectBtn.addEventListener("click", function (event) {
  event.preventDefault();

  const projectName = document.getElementById("project").value;
  const id = crypto.randomUUID();
  const newProject = new Project(projectName, id);

  addProject(newProject);
  makeProject(projectName, id);
});

// 通过事件委托处理现有和后续创建的项目。
document.addEventListener("click", function (event) {
  if (!(event.target instanceof Element)) {
    return;
  }

  const projectElement = event.target.closest(".overview");
  if (!projectElement) {
    return;
  }

  const projectId = projectElement.getAttribute("data-id");
  selectedProject = findProject(projectId);

  if (!selectedProject) {
    return;
  }

  document.querySelector(".hide").style.display = "block";
  changeProject(selectedProject.title);
});

// 添加任务:监听器只注册一次。
taskBtn.addEventListener("click", function (event) {
  event.preventDefault();

  if (!selectedProject) {
    return;
  }

  const name = document.getElementById("task").value;
  const priority = document.getElementById("priority").value;
  const date = document.getElementById("date").value;
  const info = document.getElementById("info").value;
  const taskId = crypto.randomUUID();

  const task = new ListItems(name, priority, date, info, taskId);

  selectedProject.addListItem(task);
  makeTask(name, date, taskId);
});

closest(".overview") 可以处理点击项目内部标题等子元素的情况。示例将监听器绑定在 document 上;如果页面有固定的项目列表容器,也可以绑定到该容器,缩小委托范围。

选择项目时,代码会更新外层的 selectedProject。添加任务的回调读取这个变量,就能取得当时选中的项目,无需为每个项目重新注册任务按钮事件。

确认数据保存与页面显示各执行一次

原片段创建了 task 对象,但没有显示调用 Project.addListItem(task)。示例补上了这一步,把任务保存到当前项目的 list 中。

由于没有提供 makeTask 的实现,需要确认它的职责。如果它已经保存了任务,应把保存操作统一到一处,避免重复添加。

切换项目时,如果需要展示该项目已有的任务,还需要根据 selectedProject.list 更新任务区域。提供的片段只调用了 changeProject(result.title),无法判断是否已经实现任务列表切换。

其他需要检查的细节

回调应显式接收 event,例如 function (event)。原代码直接使用 event.preventDefault(),依赖外部或浏览器提供的 event,容易产生兼容性问题。

日期处理也需要单独确认:

this.due = format(new Date(), dues)

传给 format 的日期始终是 new Date(),用户输入的 dues 则作为第二个参数。由于片段没有给出 format 的来源,无法确定参数是否符合该函数的要求。如果只需要保存日期输入框的原始值,可以改为:

this.due = dues;

日期处理与重复绑定监听器是两个独立问题。

如何验证修复结果?

替换旧事件代码并重新加载页面后,可以按以下顺序检查:

  1. 创建 4 个项目,选中其中一个项目。
  2. 添加一个任务,确认页面只新增一项,该项目的 list 长度只增加 1。
  3. 切换到另一个项目并添加任务,确认任务保存在新选中的项目中。
  4. 在任务按钮回调里临时添加 console.count("add task handler"),确认一次点击只执行一次回调。

备注:内容仅供参考。