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 = '',但这导致所有待办事项变为空。请问我哪里出错了?如何重置对象属性且不影响已推入数组的元素?
问题分析与修复方案
错误原因
- 状态与DOM不同步:你直接操作
document.getElementById('toDoText').value修改表单值,但组件的text状态(通过v-model绑定)并未同步更新。下次提交时,传递的text还是之前的有效值,导致验证错误通过。 - 对象引用共享:
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
相关产品推荐
相关产品推荐

