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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.12 04:54:02