Vue3 循环里为什么一定要加key?选错了会怎样?
写Vue3列表渲染代码时,省事儿直接用数组索引当key,或者干脆忘记写key,打开浏览器控制台就看到Vue3的警告飘红,心里慌慌的但又觉得“反正列表能出来”?要是你刚好遇到过列表项突然错位、输入框内容乱跳、组件状态丢失的bug,大概率就是key没搞明白在搞鬼,今天咱们就掰开揉碎聊Vue3的key——从它在底层到底干了啥,到不同场景选啥值最稳妥,再对比下和Vue2的细微变化,连踩过坑的开发者常犯的错误都给你列出来,保证看完再也不被key的问题坑第二次。
key到底是个什么“身份卡”?
别把key想得太玄乎,它其实就是Vue3给每个虚拟DOM节点贴的专属“身份证”。
你想想,你平时用v-for渲染列表时,不是直接改页面上的真实DOM,而是先在Vue3的内存里生成一套虚拟DOM树——虚拟DOM说白了就是用JavaScript对象模拟出来的真实DOM的“预览版”,轻得很,修改起来比直接碰真实DOM快几百倍,那Vue3怎么知道“预览版”里的哪个节点对应页面上已经存在的真实节点呢?靠的就是key这个唯一标识:如果两个虚拟DOM节点的key完全一样,tag也没换,属性也没删没大改,Vue3就会直接复用原来的真实DOM,只更新有变化的属性,不会把整个节点拆了重写;如果key不一样,那Vue3就会觉得“这是个全新的家伙”,直接把旧节点从真实DOM里删掉,插入新生成的节点。
为啥要搞这套复用逻辑?因为真实DOM的操作(创建、删除、移动、重排)是浏览器渲染里最耗性能的环节——据很多前端性能测试的数据,一次真实DOM的重排时间可能是JavaScript执行时间的几十甚至上百倍,重绘虽然快一点,但如果次数多了也会让页面卡顿,key就是帮Vue3精准找到哪些DOM可以复用、哪些必须动、哪些完全不用碰的核心工具,能让页面渲染和更新的效率提升好几个档次。
为啥Vue3控制台总警告“v-for必须加key”?
很多刚上手Vue3的开发者可能会发现:Vue2有时候不加key(比如静态列表,或者非常非常短的数组),控制台不会飘红警告;但Vue3只要写v-for循环,不管循环啥,不管数组长度多少,只要没加key,警告肯定准时出现——甚至在Vue3的TS语法中,不加key直接编译报错,这不是Vue3变“矫情”了,而是它的底层diff算法(也就是对比新旧虚拟DOM树的算法)升级了,对key的依赖更强了。
先唠唠Vue2和Vue3的diff算法有啥不一样
Vue2的diff算法是“双端比较法”,简单说就是用四个指针,分别指向新旧虚拟DOM列表的头和尾,逐个对比:头头对、尾尾对、头尾对、尾头对,找到可以复用的就直接移动,找不到的才新增或删除,这种算法在处理头尾变化不大、只有中间几个元素移动的列表时效率很高,但在处理完全倒序、或者大量随机插入/删除的列表时,四个指针来回跳,比较次数会非常多,性能提升就有限了。
Vue3的diff算法改成了“快速双端比较法”,核心思路是先处理首尾相同的元素,跳过相同的头尾,然后给剩下的旧虚拟DOM建立一个「key到索引」的映射表,接着遍历新虚拟DOM剩下的部分,拿着新虚拟DOM的key去映射表里找对应的旧索引——找得到就复用并标记,找不到就新增;最后再把旧虚拟DOM里没被标记的节点统统删掉,这种算法在处理各种场景的列表时都更高效,尤其是完全倒序或者大量随机操作的场景,比Vue2快了不止一倍。
但你注意到了吗?Vue3的diff算法里有个关键步骤——建立「key到索引」的映射表,如果没有key,这个映射表就建不起来,Vue3只能退而求其次用“双端比较法”甚至更原始的“逐项替换法”,性能会大打折扣;而且更重要的是,没有唯一标识的话,Vue3根本不知道哪个旧节点对应哪个新节点,很容易发生“状态错位”“内容乱跳”的bug,所以Vue3才强制要求v-for必须加key。
用数组索引当key会踩哪些大坑?
既然必须加key,那省事儿直接用数组的index当key行不行?很多时候能凑合用,但遇到下面这几种场景,绝对会让你头大。
在列表中间插入或删除元素
比如你有一个用户列表,数据是['张三', '李四', '王五'],v-for循环的时候用index当key:
- 张三的key是0,李四是1,王五是2
- 对应真实DOM的节点1、节点2、节点3
现在你在张三和李四中间插入了一个新用户“赵六”,数据变成了['张三', '赵六', '李四', '王五'],这时候每个元素的index都变了:
- 张三的key还是0(没问题)
- 赵六的key变成了1
- 李四的key变成了2
- 王五的key变成了3
那Vue3的diff算法会怎么处理?它拿着新虚拟DOM的key去旧映射表里找:
- key0对应旧索引0,复用节点1,没问题
- key1对应旧索引1,但旧索引1的真实DOM节点2原来属于李四,现在Vue3以为这个节点属于赵六,直接把节点2的内容改成“赵六”,输入框(如果有的话)的状态却还是李四的
- key2对应旧索引2,旧索引2的节点3原来属于王五,现在被改成了“李四”,输入框状态还是王五的
- key3找不到旧映射,新增节点4,写“王五”
结果就是:插入一个赵六,后面三个节点的内容全被强制修改,输入框内容乱跳,组件内部的状态(比如复选框的勾选状态、折叠面板的展开状态)也跟着错位——这就是典型的“状态污染”。
对列表进行倒序、打乱顺序的操作
这个场景和插入删除有点像,比如你原来的列表顺序是张三(0)、李四(1)、王五(2),倒序之后变成王五(0)、李四(1)、张三(2),这时候每个元素的index也全变了,Vue3的diff算法会拿着新的key去找旧的映射,找不到对应关系的节点会被直接替换,明明只是移动了几个元素的位置,却变成了创建三个新节点、删除三个旧节点,性能直接拉胯不说,状态也会乱。
列表项有子组件,且子组件有本地状态
比如你写了一个todoList,每个todo项都是一个独立的子组件,子组件里有一个“是否已完成”的本地state,还有一个输入框可以修改todo内容,这时候你要是用index当key,不管是插入删除还是倒序,子组件的本地state都会完全错位——比如你勾选了原来的第二项李四,插入赵六之后,第二项赵六的复选框会自动被勾选,输入框里还留着李四修改过的内容,这完全不是我们想要的效果。
那什么时候用数组index当key才不会出问题?只有一种情况:你的列表是完全静态的,不会发生任何插入、删除、倒序、打乱顺序的操作,而且列表项里没有任何有本地状态的子组件或表单元素——比如网站底部的版权信息列表,永远都是那几条,顺序也不会变,这时候用index当key是可以的,甚至性能和用唯一ID差不多,但我还是建议你尽量养成用唯一ID当key的习惯,万一哪天产品经理说“版权信息加个可拖拽排序的功能”,你不用改代码就能直接上线。
Vue3 key的正确选值是什么?
说了这么多用index当key的坑,那到底选啥值当key才是最稳妥的?记住一句话:选列表项数据里唯一的、稳定的、不会变化的标识符。
首选:后端返回的唯一ID
后端接口返回的列表数据里,每个元素都会有一个唯一的ID——比如用户的user_id、订单的order_id、文章的article_id,这些ID是数据库生成的,绝对唯一,而且不会因为列表的插入、删除、倒序而改变,这是最完美的key值。
// 模拟后端返回的用户列表数据
const userList = ref([
{ user_id: 1001, name: '张三', age: 28 },
{ user_id: 1002, name: '李四', age: 25 },
{ user_id: 1003, name: '王五', age: 30 }
])
<!-- v-for循环时用user_id当key -->
<div v-for="user in userList" :key="user.user_id">
<p>{{ user.name }} - {{ user.age }}岁</p>
<input type="text" placeholder="修改姓名" />
<TodoItem :todo-list="user.todoList" />
</div>
这样不管你对userList做什么操作,Vue3的diff算法都能精准找到对应的真实DOM,复用正确的节点,状态也不会乱,性能也能达到最优。
次选:自己生成的唯一标识
有时候后端返回的数据里没有现成的唯一ID——比如前端自己生成的临时列表、或者从本地存储里读取的没有ID的数据,这时候你就需要自己生成一个唯一的标识。
生成唯一标识的方法有很多,
- 用时间戳+随机数:这种方法生成的ID在大多数场景下都是唯一的,但如果是极短时间内生成大量元素(比如循环生成1000个元素),时间戳可能会重复,随机数也有极小的概率重复,不过一般临时用用没问题。
- 用第三方库的UUID生成函数:比如uuid库的v4()函数,可以生成全球唯一的标识符,绝对不会重复,非常适合需要长期存储或者高并发生成元素的场景。
- 用列表项的多个属性拼接成唯一字符串:如果列表项的多个属性组合起来是唯一的,也可以拼接起来当key——比如某个商品列表里,没有商品ID,但每个商品的品牌+型号+颜色组合起来是唯一的,就可以用
${item.brand}-${item.model}-${item.color}当key。
比如用uuid库生成唯一ID:
import { v4 as uuidv4 } from 'uuid'
// 前端自己生成的临时todo列表
const tempTodoList = ref([
{ id: uuidv4(), content: '写Vue3 key的文章', done: false },
{ id: uuidv4(), content: '吃午饭', done: false },
{ id: uuidv4(), content: '睡午觉', done: false }
])
// 新增todo项时也用uuid生成ID
const addTodo = () => {
tempTodoList.value.push({
id: uuidv4(),
content: '新的待办事项',
done: false
})
}
绝对不能选的值
除了刚才说的数组index(特殊场景除外),还有几个值绝对不能选当key:
- 随机数:每次页面刷新或者列表重新渲染,随机数都会变,这会导致Vue3找不到任何可以复用的旧节点,直接把整个列表拆了重写,性能比不加key还差,状态也会完全重置。
- null或undefined:Vue3会直接把这种key当成“没有key”,然后强制加警告,退回到原始的diff算法。
- 列表项的内容本身:如果列表项的内容可能会变化,那千万不能用内容当key——比如刚才的todoList,如果你用content当key,修改了todo内容之后,key就变了,Vue3会把原来的todo项删掉,插入一个新的todo项,输入框的内容虽然能保持(因为你刚改了),但组件的本地state(比如done的勾选状态)会重置。
Vue3 key除了用在v-for,还有别的用处吗?
很多开发者以为key只能用在v-for循环里,其实不然——key在Vue3里还有几个非常实用的隐藏用法,学会了能解决很多棘手的问题。
强制销毁并重建组件
比如你有一个表单组件,里面有很多表单元素,用户填了一半之后,产品经理要求加一个“重置表单”的按钮,而且重置的时候不仅要清空表单内容,还要重置组件内部的所有本地state(比如表单验证的错误状态、自定义的临时变量),这时候你用表单组件的reset方法可能只能清空内容,不能重置本地state;如果你直接用v-if切换组件的显示隐藏,Vue3可能会复用原来的组件,本地state还是保留的,这时候你就可以给表单组件加一个key,每次点击“重置表单”的时候,改变这个key的值——Vue3看到key变了,就会直接销毁原来的组件,插入一个全新的组件,所有的本地state都会重置成初始值,非常好用。
<!-- 给Form组件加key,key值是resetCount --> <Form :key="resetCount" /> <button @click="resetForm">重置表单</button>
const resetCount = ref(0)
const resetForm = () => {
// 每次点击重置,resetCount加1,key值就变了
resetCount.value++
}
解决异步组件的加载闪烁问题
有时候你用Vue3的defineAsyncComponent加载异步组件时,可能会出现加载闪烁的问题——比如异步组件加载完成之前,页面上会显示loading组件,加载完成之后会突然替换成异步组件,看起来很生硬,这时候你可以给异步组件和loading组件都加一个相同的key,Vue3就会认为这两个组件是同一个“身份”的不同状态,会进行平滑的过渡,不会出现闪烁。
控制v-if和v-for的优先级
哦对了,这里顺便提一下Vue3里v-if和v-for的优先级变化:Vue2里v-for的优先级比v-if高,所以如果你在同一个元素上同时写v-if和v-for,Vue2会先循环所有元素,再对每个元素进行判断;但Vue3里v-if的优先级比v-for高,所以如果你在同一个元素上同时写v-if和v-for,Vue3会先判断v-if的条件,如果条件不满足,循环就不会执行——但这时候你必须注意v-if里用到的循环变量,因为v-if先执行,循环变量还没定义,所以会报错,这时候你可以用template标签把v-for包裹起来,给template加key,然后在里面的元素上写v-if,这样既不会报错,又能控制优先级。
不过不管是Vue2还是Vue3,官方都不建议在同一个元素上同时写v-if和v-for,因为这样会造成性能浪费——Vue2里是先循环再判断,浪费了循环的时间;Vue3里虽然先判断,但有时候循环变量必须用到,还是得用template包裹。
写在最后
总结一下今天的内容:
- Vue3的key是每个虚拟DOM节点的专属“身份证”,能帮助Vue3的diff算法精准找到可以复用的真实DOM,提升页面渲染和更新的效率,避免状态错位。
- Vue3强制要求v-for必须加key,因为它的diff算法升级了,对key的依赖更强了。
- 数组index当key只有在完全静态的列表里才不会出问题,其他场景尽量用后端返回的唯一ID,或者自己生成的UUID。
- key除了用在v-for循环里,还能强制销毁并重建组件、解决异步组件的加载闪烁问题、控制v-if和v-for的优先级。
key虽然只是Vue3里的一个小属性,但它的作用却非常大——很多前端新手甚至老手都会在key的问题上踩坑,所以一定要把它搞明白,希望今天的文章能帮到你,以后写Vue3代码的时候再也不被key的警告飘红和bug困扰啦!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

