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

Alpine.js删除对象数组元素后页面渲染异常问题

问题

我开发的待办任务清单应用遇到渲染bug:删除任务时,localStorage里的数组已正确删除对应对象,但页面渲染时总是删掉最后一项,刷新或添加新任务后显示才恢复正常。

相关代码细节:

  • 删除函数:async deleteTask(taskList, index){taskList = await taskList.splice(index,1)}
  • 渲染用x-for="(task, index) in filteredTasks" :key="index"
  • 删除后已更新filteredTasks:x-on:click=" await deleteTask(taskList, index); filteredTasks = [...taskList]"

完整代码:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Task List</title>
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
    />

    <!-- <script src="https://cdn.tailwindcss.com"></script> -->
    <script
      defer
      src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"
    ></script>
    <script
      defer
      src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
    ></script>

    <script>
      function methods() {
        return {
          async pushTask(taskList) {
            if (document.getElementById("taskTitleInput").value === "") {
              document.getElementById("taskTitleInput").value = "No Title";
            }
            if (document.getElementById("taskDescriptionInput").value === "") {
              document.getElementById("taskDescriptionInput").value =
                "No Description";
            }
            taskList.push({
              title: document.getElementById("taskTitleInput").value,
              description: document.getElementById("taskDescriptionInput")
                .value,
              completed: false,
            });
            document.getElementById("taskTitleInput").value = "";
            document.getElementById("taskDescriptionInput").value = "";
          },
          async firstTask(taskList) {
            if (taskList.length === 0) {
              taskList.push({
                title: "Add a Task",
                description: "Add your first task to get started!",
                completed: false,
              });
            }
          },
          async deleteTask(taskList, index){
            taskList = await taskList.splice(index,1)
          }
        };
      }
    </script>
    <style>
      [x-cloak] {
        display: none;
      }
    </style>
  </head>
  <body x-data="methods()">
    <h1>Personal Task Manager</h1>
    <div
      aria-label="Task Manager App"
      x-data="{ taskList: $persist([]), filteredTasks: [] }"
      x-directives="{persist: $persist}"
    >
      <form aria-label="New Task Form">
        <label for="taskTitle">Task title:</label
        ><input type="text" id="taskTitleInput" name="taskTitle" value="" />
        <label for="taskDescription">Task description:</label
        ><input
          type="text"
          id="taskDescriptionInput"
          name="taskDescription"
          value=""
        />
        <button
          x-on:click="await pushTask(taskList); $nextTick(()=>{filteredTasks = [...taskList]})"
        >
          Add Task
        </button>
      </form>
      <div
        aria-label="Task List"
        x-init="$nextTick(()=>{filteredTasks = [... taskList]})"
      >
        <div aria-label="Filters">
          <button aria-label="All" x-on:click="filteredTasks = [...taskList]">
            All</button
          ><button
            aria-label="Incomplete"
            x-on:click="filteredTasks = taskList.filter(task => task.completed === false
        )"
          >
            Incomplete</button
          ><button
            aria-label="Complete"
            x-on:click="filteredTasks = taskList.filter(task => task.completed === true)"
          >
            Complete
          </button>
        </div>
        <!-- Another div with additional features here -->
        <div aria-label="Tasks" x-init="firstTask(taskList)">
          <template x-for="(task, index) in filteredTasks" :key="index">
            <div x-data="{open: false}">
              <div x-show="!open">
                <input
                  x-on:click="task.completed = !task.completed"
                  type="checkbox"
                  x-bind:id="`checkbox-${index}`"
                  x-bind:value="task.completed"
                />
                <h3 x-text="task.title"></h3>
                <p x-text="task.description"></p>
              </div>
              <form x-show="open">
                <label for="editTitle">Edit title:</label
                ><input
                  type="text"
                  x-bind:id="`editTitleInput-${index}`"
                  name="editTitle"
                  x-model:placeholder="task.title"
                />
                <label for="editDescription">Edit description:</label
                ><input
                  type="text"
                  x-bind:id="`editDescriptionInput-${index}`"
                  name="editDescription"
                  x-model="task.description"
                />
                <button
                  x-on:click="open = false; $nextTick(()=>{filteredTasks = [...taskList]})"
                >
                  Submit Changes
                </button>
              </form>

              <button x-on:click="open = ! open" aria-label="Edit Task">
                <span class="material-symbols-outlined"> edit </span>
              </button>
              <button
                aria-label="Delete Task"
                x-on:click=" await deleteTask(taskList, index);
                filteredTasks = [...taskList]"
              >
                <span class="material-symbols-outlined"> close </span>
              </button>
            </div>
          </template>
        </div>
      </div>
    </div>
  </body>
</html>

已尝试的方法均无效:

  • 将删除逻辑内联,问题依旧;
  • 切换x-for基于taskList渲染,问题完全相同;
  • 改用filter替代splice删除任务,渲染问题仍存在。

解决方案

1. 核心问题:x-for使用索引作为key

AlpineJS依靠:key值追踪列表元素的身份,用数组索引作为key时,删除元素后索引会重排,导致框架无法正确识别哪个元素被删除,进而渲染错误。必须使用每个任务的唯一标识作为key。

2. 修复步骤

步骤1:给每个任务添加唯一ID

修改pushTask和firstTask方法,生成唯一ID:

function methods() {
  return {
    async pushTask(taskList) {
      let title = document.getElementById("taskTitleInput").value || "No Title";
      let description = document.getElementById("taskDescriptionInput").value || "No Description";
      taskList.push({
        id: crypto.randomUUID(), // 添加唯一ID
        title: title,
        description: description,
        completed: false,
      });
      document.getElementById("taskTitleInput").value = "";
      document.getElementById("taskDescriptionInput").value = "";
    },
    async firstTask(taskList) {
      if (taskList.length === 0) {
        taskList.push({
          id: crypto.randomUUID(), // 添加唯一ID
          title: "Add a Task",
          description: "Add your first task to get started!",
          completed: false,
        });
      }
    },
    // ...其他方法
  };
}

步骤2:修改x-for的key为任务ID

把渲染模板的key从索引改为任务的唯一ID:

<template x-for="(task, index) in filteredTasks" :key="task.id">

步骤3:修复deleteTask函数

splice是同步操作,不需要async/await,且不能将splice的返回值赋值给taskList(splice返回的是被删除的元素数组,会覆盖原数组引用):

deleteTask(taskList, index){
  taskList.splice(index, 1);
}

或者更稳妥的方式,用任务ID来删除(避免索引可能的偏差):

deleteTask(taskList, taskId){
  const taskIndex = taskList.findIndex(task => task.id === taskId);
  if(taskIndex !== -1){
    taskList.splice(taskIndex, 1);
  }
}

对应的点击事件也要调整为传入任务ID:

<button
  aria-label="Delete Task"
  x-on:click="deleteTask(taskList, task.id); filteredTasks = [...taskList]"
>
  <span class="material-symbols-outlined"> close </span>
</button>

3. 可选优化:用x-model替代DOM查询

AlpineJS推荐用x-model绑定输入值,替代getElementById,让代码更简洁且符合框架规范:

  1. 在应用的x-data中添加输入绑定变量:
<div
  aria-label="Task Manager App"
  x-data="{ taskList: $persist([]), filteredTasks: [], newTaskTitle: '', newTaskDescription: '' }"
  x-directives="{persist: $persist}"
>
  1. 修改输入框为x-model绑定:
<input type="text" id="taskTitleInput" name="taskTitle" x-model="newTaskTitle" />
<input type="text" id="taskDescriptionInput" name="taskDescription" x-model="newTaskDescription" />
  1. 更新pushTask方法:
async pushTask(taskList, newTaskTitle, newTaskDescription) {
  let title = newTaskTitle || "No Title";
  let description = newTaskDescription || "No Description";
  taskList.push({
    id: crypto.randomUUID(),
    title: title,
    description: description,
    completed: false,
  });
}
  1. 修改添加任务的点击事件:
<button
  x-on:click="await pushTask(taskList, newTaskTitle, newTaskDescription); newTaskTitle=''; newTaskDescription=''; $nextTick(()=>{filteredTasks = [...taskList]})"
>
  Add Task
</button>

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 23:27:51