VUE学习|使用v-for和checkbox中遇到的问题

3,391 阅读3分钟

本文记录了我在实现一个简单的TodoList的过程中遇到的问题即解决方法。由于我目前水平较低,仍有未明白的地方,同时文中也可能出现纰漏或者错误指出,若是有人可以看到此文,希望可以解答我的疑惑或者指出不正之处,谢谢。

需求

在一个简单的TodoList应用中,使用v-for指令绑定待办事项列表进行渲染。待办事项列表中每个对象有一个Boolean类型的属性表明该事项是否已经完成,在页面中通过v-model将该属性与一个checkbox进行双向绑定,实现通过勾选设置事项是否完成。

现在要实现的是将用户勾选标明已完成的事项放到下面,也就是在点击checkbox后调整数组的位置,实现所有的已完成事项都出现在未完成事项的下面。同时,如果重新将已完成的事项取消勾选,其又会上升。

原本的做法如下:

<ul>
    <li v-for="item in todoList">
        <input type="checkbox" v-model="item.isFinished" @change="moveEvent(item, $event)">
        <span :class="{finished: item.isFinished}">{{ item.name }}</span>
        <button @click="removeEvent(item)">删除</button>
        <button @click="topEvent(item)">置顶</button>
    </li>
</ul>

moveEvent()函数如下,做法比较粗暴,先从数组中移除原来的对象,然后找到合适的位置再加进去。

moveEvent(item, event){
    console.log(event.target.checked)
    if(event.target.checked){
        this.todoList.splice(this.todoList.indexOf(item), 1);
        var index;
        for(index = 0; index < this.todoList.length; index++){
            if(this.todoList[index].isFinished == true){
                break;
            }
        }
        this.todoList.splice(index, 0, item);
    }else{
        this.todoList.splice(this.todoList.indexOf(item), 1);
        var index;
        for(index = 0; index < this.todoList.length; index++){
            if(this.todoList[index].isFinished == true){
                break;
            }
        }
        this.todoList.splice(index, 0, item);
    }
}

出现的问题

初始状态

当我点击列表的第一个元素的checkbox时,出现如下图的情况:

发现,虽然第一个事项是被移到了下面,并且成功修改为已完成,但是随后由于修改了数组产生的新的第一个元素的checkbox也被勾选上了,但是没有出现已完成的CSS效果,并且刷新后就可以正常显示,说明其实际上并没有被修改为已完成。

同样的,取消勾选会导致相反的情况,同样刷新后即会正常显示。

原因分析

可以判断我们的数据并没有出错,并且数组元素的移动也是正常的,但是我们的checkbox明明通过v-model进行了双向绑定,为什么会出现显示错误的情况呢?经过定位,最终将问题锁定在v-for指令上。先来看看官网对于v-for指令的介绍:

当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。这个类似 Vue 1.x 的 track-by="$index"。

其实这也是一个老生常谈的问题了,就是说当我们改变了数据项顺序,Vue并没有移动DOM元素,而是重新渲染每个DOM上的数据,这样的目的应该是为了减少DOM操作,提高效率。

那么回看我们的问题,大致判断原因应该就是出于此:假设我们把当前索引为0的元素标记为已完成,该元素被移到数组下方,并且新位置上渲染结果正确。但是此时由于重新排序而出现在索引0位置的新元素的checkbox却由于DOM重用的结果,没有得到更新,后面的解决方案也验证了是用于这个原因造成的。

但是我是进行了v-model绑定了的,为何还是会出现这种情况我还没有完全明白,在网上查了也没有相似情况,若有看到此处的人明白的,还请不吝赐教

解决方案

对应的,Vue官网给出了解决方法:

为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key 属性。

建议尽可能在使用 v-for 时提供 key attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。

那么尝试将v-for改为:

<li v-for="(item, index) in todoList" :key="index">

结果发现,还是不行。我认为原因应该是这样的写法用数组的地址作为元素的身份,那么原先被标记为0(即数组中索引为0的元素)被成功勾选并下移,而新的数组头元素此时也被标记为0,两者身份相同,所以又会错误的被勾上!

正确写法(错误写法)如下,这里我使用了元素的name属性,即事件的名称:

<li v-for="item in todoList" :key="item.name">

尴尬😅,本来以为上面的写法是正确的,但是在写本文的时候写着写着突然觉得不对,既然索引会出现错误,那么具有相同name属性的事件是不是也会出现错误?去试验了一下,果不其然...但还是记录下来给自己提个醒。

那么,经过这么多次试验得出的结论就是需要使用唯一的key属性,才能保证万无一失,例如给每个事件一个不会重复的id