Vue TodoList中v-model绑定复选框异常:已完成视图取消勾选后显示错误
解决TodoList已完成视图中取消勾选后复选框显示异常的问题
这个问题的根源很明确——你在使用Vue的v-for渲染待办项列表时,没有指定唯一的key属性,导致Vue的虚拟DOM复用逻辑出现了混乱。
问题原因
当你在Completed视图中取消某个已完成项的勾选时,这个项会被filters.completed过滤器排除出当前显示的列表。由于Vue默认会复用DOM元素(没有key的情况下),它会把下一个待办项的数据绑定到之前被移除项的DOM节点上,但复选框的状态没有被正确重置,从而出现显示异常。
解决方案
我们需要给每个待办项添加唯一标识,并在v-for中指定这个标识作为key,让Vue能准确跟踪每个节点的身份。
步骤1:给新增的待办项添加唯一ID
修改addItem方法,在创建待办项时加入一个唯一的id字段(这里用时间戳简单实现,也可以用自增ID):
addItem: function (todo) { var value = this.newTodo && this.newTodo.trim(); if (!value) return; this.todos.push({ id: Date.now(), // 添加唯一ID content: value, completed: false }); this.newTodo = ''; }
步骤2:给v-for循环的元素添加key属性
修改HTML中的待办项列表,给<li>标签加上:key="todo.id":
<ul class="todo-list"> <li v-for="todo in todosForShow" :key="todo.id" :class="{completed: todo.completed}"> <input type="checkbox" v-model="todo.completed"> {{todo.content}} - <a href="#" @click.prevent="deleteItem(todo)">remove</a> </li> </ul>
为什么这样有效?
Vue通过key属性来识别每个DOM节点对应的数据源项,当列表数据发生变化时,它会基于key的变化来重新排列元素顺序,而不是复用原来的DOM元素,这样就能保证复选框的状态和对应的数据项始终保持一致,不会出现显示异常的问题。
修改后的完整代码(关键部分已标注)
JavaScript部分
var STORAGE_KEY = 'todo mvc by vue'; var todoStorage = { data: [], fetch: function () { return this.data; }, save: function (data) { this.data = data; //localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } } var filters = { all: function (todos) { return todos; }, active: function (todos) { return todos.filter(todo => !todo.completed); }, completed: function (todos) { return todos.filter(todo => todo.completed); } } var app = new Vue({ el: '.app', data: { todos: todoStorage.fetch(), newTodo: '', visiblitys: [{ title: 'All', selected: true, filter: filters.all }, { title: 'Active', selected: false, filter: filters.active }, { title: 'Completed', selected: false, filter: filters.completed }] }, computed: { todosForShow: function () { return this.currentFilter(this.todos); }, currentFilter: function () { return this.visiblitys.find(v => v.selected).filter; } }, watch: { todos: { handler: function (todos) { todoStorage.save(todos); }, deep: true } }, methods: { deleteItem: function (todo) { this.todos.splice(this.todos.indexOf(todo), 1); }, deleteCompletedItem: function () { this.todos = filters.active(this.todos); }, addItem: function (todo) { var value = this.newTodo && this.newTodo.trim(); if (!value) return; this.todos.push({ id: Date.now(), // 新增唯一ID content: value, completed: false }); this.newTodo = ''; }, selectVisiblity: function (visiblity) { this.visiblitys.forEach(vis => vis.selected = false); visiblity.selected = true; }, completeAll: function () { this.todos.forEach(todo => todo.completed = true); } } });
HTML部分
<header> <h1>Todos</h1> </header> <div class="app container"> <input class="new-todo" type="text" v-on:keyup.enter="addItem()" v-model="newTodo" placeholder="What needs to be done?"> <section class="main" v-show="todos.length > 0"> <button class="complete-all" @click="completeAll()">Complete All</button> <ul class="todo-list"> <!-- 添加:key="todo.id" --> <li v-for="todo in todosForShow" :key="todo.id" :class="{completed: todo.completed}"> <input type="checkbox" v-model="todo.completed"> {{todo.content}} - <a href="#" @click.prevent="deleteItem(todo)">remove</a> </li> </ul> <footer> <span class="todo-count">{{todos.filter(t=>!t.completed).length}} items left</span> <ul class="filters"> <li v-for="visiblity in visiblitys"> <a href="#" :class="{selected: visiblity.selected}" @click.prevent="selectVisiblity(visiblity)">{{visiblity.title}}</a> </li> </ul> <button class="clear-completed" v-show="todos.some(t=>t.completed)" @click="deleteCompletedItem()">Clear Completed</button> </footer> </section> <br>todos raw data:{{this.todos}} </div> <script src="https://cdn.jsdelivr.net/npm/vue"></script>
CSS部分(无修改)
* { box-sizing: border-box; } header h1 { text-align: center; } .container { max-width: 800px; margin: 0 auto; } .new-todo { padding: 16px; box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); font-size: 24px; width: 100%; } li.completed { text-decoration: line-through; } footer { border-top: 1px solid #e6e6e6; height: 20px; padding: 10px 15px; color: #777; text-align: center; position: relative; } .filters { list-style: none; margin: 0; padding: 0; position: absolute; left: 0; right: 0; } .filters li { display: inline; } .filters li a { margin: 3px; padding: 3px 7px; text-decoration: none; border: 1px solid transparent; border-radius: 3px; color: inherit; } .filters li a.selected { border-color: rgba(175, 47, 47, 0.2); } .todo-count { float: left; text-align: left; } .complete-all { float: right; cursor: pointer; position: relative; color: black; } .todo-list { clear: both; } .clear-completed { float: right; cursor: pointer; position: relative; } button { margin: 0; padding: 0; border: 0; background: none; font-size: 100%; vertical-align: baseline; font-family: inherit; font-weight: inherit; color: inherit; -webkit-font-smoothing: antialiased; }
现在测试一下,在Completed视图中取消勾选任意已完成项,剩余项的复选框就会正常显示了。
内容的提问来源于stack exchange,提问作者Yu-Lin Wang
相关产品推荐
相关产品推荐

