v-for 中 key 的作用是什么?为什么不推荐用 index?
简化版
key 用来标识列表节点身份,帮助 Vue 在 Diff 时复用和移动正确的 DOM 或组件实例。不推荐用 index 作为 key,因为插入、删除、排序后 index 会变化,可能导致状态错乱和错误复用。
详细版
推荐:
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
不推荐:
<li v-for="(item, index) in list" :key="index">
如果列表只是静态展示、不排序不增删,用 index 问题不大。但动态列表、表单项、组件列表中,必须用稳定唯一 id。
key 的作用不是让渲染一定更快,而是让节点身份更准确,减少错误复用。
完整版教学
一、Diff 需要判断谁是谁
列表更新时,Vue 要比较新旧 vnode,决定复用、移动、创建还是删除。key 就像每个节点的身份证。
没有稳定 key,框架只能按位置猜测,可能把旧节点复用到错误数据上。
二、index 作为 key 的问题
假设列表 [A, B, C] 用 index 作为 key,删除 A 后变成 [B, C]。B 的 index 从 1 变成 0,C 从 2 变成 1。
框架看到 key 0 还在,可能把原来 A 的 DOM/组件实例复用给 B。这在输入框列表中会出现输入内容错位。
三、什么时候 index 可以接受
如果列表完全静态,不会插入、删除、排序,也没有子组件内部状态,用 index 风险较低。
但面试和工程最佳实践都建议用业务唯一 id,这样未来需求变化也更安全。
是否可用 index 要同时满足“顺序固定、数量固定、节点无需要跟随业务项的状态”。这三个条件任意一个可能变化,就应在数据进入视图前准备稳定 id,而不是等输入错位后再补 key。
四、key 和性能的关系
key 不只是性能优化标记。错误 key 会导致错误复用;正确 key 能让 Diff 更准确地移动节点。
有时加 key 反而会让节点重建,比如你故意改变组件 key 来重置组件状态。这也是 key 控制身份的体现。
五、面试追问与工程落地
key 常见追问是“为什么有时故意改变 key”。因为 key 决定组件身份。给表单、弹窗、详情组件换 key,可以强制 Vue 销毁旧实例并创建新实例,从而重置内部状态。这是 key 的主动使用场景。
还会问“不写 key 会怎样”。Vue 会尽量按就地更新策略复用节点,简单静态列表可能没问题,但有状态子组件、输入框、动画列表、排序增删时容易出现错位。稳定 key 是动态列表的基本要求。
工程里 key 要唯一且稳定。不要用随机数当 key,因为每次渲染都变,等于告诉 Vue 所有节点都不是原来的,会导致频繁销毁重建,性能和状态都会受影响。
六、从 keyed Diff 的匹配与移动理解身份
keyed Diff 会先复用新旧两端可直接匹配的节点,再为中间未知区域建立 key 到新位置的映射,判断哪些旧节点可复用、哪些应删除。为了减少真实 DOM 移动,Vue 还会根据新旧位置关系寻找可保留的最长递增子序列;所以 key 既参与正确性,也影响复用和移动决策。
旧:A(key=1) B(key=2) C(key=3) D(key=4)
新:B(key=2) D(key=4) A(key=1) C(key=3)
key 映射先确认四个身份仍存在,再决定最少移动哪些节点。
index key 只会得到 0、1、2、3,看不出业务项已经换位。
| key 选择 | 插入或排序后的身份 | 典型结果 |
|---|---|---|
| 稳定业务 id | 同一数据保持同一 key | 组件实例和 DOM 正确复用、移动 |
| 数组 index | key 跟位置而非数据走 | 输入值、局部状态可能串项 |
| 每次新随机数 | 所有 key 都变化 | 全部销毁重建,状态丢失 |
| 重复 key | 身份不唯一 | 匹配结果不可可靠推断并产生警告 |
以 [A,B,C] 删除头部 A 为例,业务 id 能明确删除 1 个节点并复用 B、C 两个实例;index key 则把旧 key 0、1 继续对应到新位置,可能把 A、B 的局部 DOM 状态分别交给 B、C。无状态纯文本看起来可能没问题,有输入框或子组件状态时就会暴露错位。
key 也可以主动改变身份。详情页从用户 1 切到用户 2 时,如果业务要求完整重置子组件本地状态,把用户 id 作为组件 key 会触发旧实例卸载、新实例挂载;这是一种明确的生命周期控制,同时要接受重建成本。
<EditorRow
v-for="item in items"
:key="item.id"
:item="item"
/>
这里 item.id 应在同一组兄弟节点中唯一,并在该实体的生命周期内稳定。不要把整个对象直接作为 key:对象 key 依赖引用身份,重新从接口组装出内容相同的新对象时引用已经变化;业务主键更能表达“仍是同一条数据”。
无 key 的就地更新并不等于每次都错。纯文本、无内部状态且顺序固定时,按位置复用可能足够;一旦列表能插入、删除、排序,或行内含输入框、动画和有状态子组件,就必须优先保证身份正确。
key 回答的是“新旧两棵树中的这两个节点是不是同一个实体”,不是简单的“有没有重复数据”或“能不能让渲染变快”。
七、常见误区与追问
- 误区:key 只是为了提高列表性能。 它首先定义节点身份,错误 key 会导致状态错位,性能只是复用和移动策略的结果。
- 误区:index 在任何 v-for 中都绝对不能用。 完全静态、无状态且永不重排的列表风险较低,但业务一旦增删排序就失去稳定身份。
- 误区:用 Math.random 生成 key 最唯一也最安全。 每次渲染都变等于身份全部失效,会造成组件反复销毁和创建。
- 追问:为什么输入框列表最容易暴露 index key 问题? DOM 值、焦点和组件局部状态会随错误实例复用留在位置上,而业务数据已经换项。
- 追问:key 必须全局唯一吗? 只需在同一组兄弟节点中唯一,并在该业务实体生命周期内保持稳定。
- 追问:为什么改变组件 key 能重置状态? Vue 将其视为不同 vnode 身份,旧组件卸载,新组件重新创建。
- 追问:keyed Diff 为什么还要计算递增子序列? 已匹配节点中保持相对递增的部分可留在原位,从而减少真实 DOM 移动次数。
八、加强记忆
key 是列表节点的身份证,index 是排队号码。队伍一变,排队号码就变了;身份证不会变。所以动态列表用稳定唯一 id。
面试作答要把“稳定身份 → 正确复用 → 必要时最少移动”这条因果链说完整,并明确随机 key 会主动切断复用。