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

Vue组合式API Todo应用:如何重置对象属性且不影响数组元素?

问题描述

我正在从Vue的选项式API过渡到组合式API,为此搭建了一个小型Todo应用。

components\AddUpdate.vue 组件代码

<template>
  <form
    class="card-footer"
    @submit.prevent="$emit('add-update-todo', text)"
    autocomplete="off"
  >
    <input id="toDoText" type="text" placeholder="Add todo" v-model="text" />
    <button type="submit">Ok</button>
  </form>
</template>

<script>
import { ref, onMounted, watch } from 'vue';
export default {
  name: 'AddUpdateTodo',

  setup() {
    const text = ref('');
    return { text };
  },
};
</script>

App.vue 代码

<template>
  <div id="app">
    <ErrorMessage
      v-if="!isValid"
      message="The todo body has to be at least 3 characters long"
    />

    <SuccessMessage v-if="allDone" message="You can relex! :)" />

    <div class="card">
      <Header />
      <List
        :todos="todos"
        @delete-todo="deleteTodo"
        @edit-todo="editTodo"
        @toggle-todo="toggleTodo"
      />
      <AddUpdateTodo @add-update-todo="AddUpdateTodo" />
    </div>
  </div>
</template>

<script>
import { ref, computed } from 'vue';
import ErrorMessage from './components/ErrorMessage.vue';
import SuccessMessage from './components/SuccessMessage.vue';
import Header from './components/Header.vue';
import List from './components/List.vue';
import AddUpdateTodo from './components/AddUpdate.vue';

export default {
  name: 'App',
  components: {
    ErrorMessage,
    SuccessMessage,
    Header,
    List,
    AddUpdateTodo,
  },

  setup() {
    const todos = ref([]);
    const currentItem = ref(null);
    const isValid = ref(true);
    const isUpdate = ref(false);
    const allDone = computed(
      () => todos.value.length && todos.value.every((todo) => todo.done)
    );

    return { todos, isValid, isUpdate, allDone };
  },

  methods: {
    toggleTodo: function (index) {
      this.todos[index].done = !this.todos[index].done;
    },

    deleteTodo: function (index) {
      if (confirm('Are you sure?')) {
        this.todos.splice(index, 1);
      }
    },

    editTodo: function (index) {
      this.isUpdate = true;
      let formField = document.getElementById('toDoText');
      // Get current Item
      this.currentItem = this.todos[index];
      // Prepopulate form with existing value
      formField.value = this.currentItem.text;
    },

    AddUpdateTodo: function (text) {
      let newTodo = {
        done: false,
        text: '',
      };

      if (text.length > 2) {
        this.isValid = true;
        if (!this.isUpdate) {
          newTodo.text = text;
          this.todos.push(newTodo);
        } else {
          this.currentItem.text = document.getElementById('toDoText').value;
          this.isUpdate = false;
        }
        document.getElementById('toDoText').value = '';
      } else {
        this.isValid = false;
      }
    },
  },
};
</script>

遇到的问题

当newTodo.text被设置为长度大于2的字符串后,后续即使表单字段为空,执行AddUpdateTodo方法时仍会满足验证条件。我尝试在this.todos.push(newTodo)后添加newTodo.text = '',但这导致所有待办事项变为空。请问我哪里出错了?如何重置对象属性且不影响已推入数组的元素?


问题分析与修复方案

错误原因

  1. 状态与DOM不同步:你直接操作document.getElementById('toDoText').value修改表单值,但组件的text状态(通过v-model绑定)并未同步更新。下次提交时,传递的text还是之前的有效值,导致验证错误通过。
  2. 对象引用共享:newTodo是一个对象,推入todos数组后,数组存储的是该对象的引用。此时修改newTodo.text = '',会直接改变数组中对应元素的text值,因为它们指向同一个内存地址。

修复步骤

1. 改造AddUpdateTodo组件,用响应式状态替代DOM操作

<template>
  <form
    class="card-footer"
    @submit.prevent="handleSubmit"
    autocomplete="off"
  >
    <input id="toDoText" type="text" placeholder="Add todo" v-model="text" />
    <button type="submit">Ok</button>
  </form>
</template>

<script>
import { ref, watch } from 'vue';
export default {
  name: 'AddUpdateTodo',
  props: {
    initialText: {
      type: String,
      default: ''
    }
  },
  setup(props, { emit }) {
    const text = ref(props.initialText);

    // 监听初始值变化,同步到组件状态
    watch(() => props.initialText, (newVal) => {
      text.value = newVal;
    });

    const handleSubmit = () => {
      emit('add-update-todo', text.value);
      // 提交后重置自身状态
      text.value = '';
    };

    // 暴露重置方法给父组件调用
    const resetText = () => {
      text.value = '';
    };

    return { text, handleSubmit, resetText };
  },
};
</script>

2. 修改App.vue,移除DOM操作,使用组件状态交互

<template>
  <div id="app">
    <ErrorMessage
      v-if="!isValid"
      message="The todo body has to be at least 3 characters long"
    />

    <SuccessMessage v-if="allDone" message="You can relex! :)" />

    <div class="card">
      <Header />
      <List
        :todos="todos"
        @delete-todo="deleteTodo"
        @edit-todo="editTodo"
        @toggle-todo="toggleTodo"
      />
      <!-- 添加ref和初始值props -->
      <AddUpdateTodo 
        ref="addUpdateRef"
        :initial-text="currentItem?.text || ''"
        @add-update-todo="AddUpdateTodo" 
      />
    </div>
  </div>
</template>

<script>
import { ref, computed } from 'vue';
import ErrorMessage from './components/ErrorMessage.vue';
import SuccessMessage from './components/SuccessMessage.vue';
import Header from './components/Header.vue';
import List from './components/List.vue';
import AddUpdateTodo from './components/AddUpdate.vue';

export default {
  name: 'App',
  components: {
    ErrorMessage,
    SuccessMessage,
    Header,
    List,
    AddUpdateTodo,
  },

  setup() {
    const todos = ref([]);
    const currentItem = ref(null);
    const isValid = ref(true);
    const isUpdate = ref(false);
    const addUpdateRef = ref(null); // 子组件引用
    const allDone = computed(
      () => todos.value.length && todos.value.every((todo) => todo.done)
    );

    return { todos, isValid, isUpdate, allDone, currentItem, addUpdateRef };
  },

  methods: {
    toggleTodo(index) {
      this.todos[index].done = !this.todos[index].done;
    },

    deleteTodo(index) {
      if (confirm('Are you sure?')) {
        this.todos.splice(index, 1);
      }
    },

    editTodo(index) {
      this.isUpdate = true;
      this.currentItem = this.todos[index];
      // 不再操作DOM,通过props同步初始值
    },

    AddUpdateTodo(text) {
      if (text.length > 2) {
        this.isValid = true;
        if (!this.isUpdate) {
          // 每次新增都创建新对象,避免引用共享问题
          const newTodo = {
            done: false,
            text: text,
          };
          this.todos.push(newTodo);
        } else {
          this.currentItem.text = text;
          this.isUpdate = false;
          this.currentItem = null;
        }
        // 调用子组件方法重置输入框
        this.addUpdateRef.value.resetText();
      } else {
        this.isValid = false;
      }
    },
  },
};
</script>

关键优化点

  • 用Vue的响应式状态替代原生DOM操作,确保组件状态与视图同步。
  • 新增待办时创建全新对象,彻底避免引用共享导致的批量修改问题。
  • 通过props和子组件暴露的方法实现父子组件状态交互,符合Vue的组件设计规范。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 10:59:51