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;
日期处理与重复绑定监听器是两个独立问题。
如何验证修复结果?
替换旧事件代码并重新加载页面后,可以按以下顺序检查:
- 创建 4 个项目,选中其中一个项目。
- 添加一个任务,确认页面只新增一项,该项目的
list长度只增加 1。 - 切换到另一个项目并添加任务,确认任务保存在新选中的项目中。
- 在任务按钮回调里临时添加
console.count("add task handler"),确认一次点击只执行一次回调。
备注:内容仅供参考。
