4.2 key 与列表渲染:复用的身份证 本节摘要:key 是 diff 在列表比对时识别"同一个节点"的标识。用稳定业务 id 与用数组索引,会在插入、删除场景产生完全不同的补丁序列。本节从双端比较讲到最长递增子序列,复盘索引 key 的三个事故现场,并澄清"用 id 就一定对"的边界情况。 能力目标 阅读完本节,你应当能够: 模拟"头插一项"时索引 key 与 id key 各自产生的补丁序列; 解释索引 key 导致输入串位、状态错乱、动画异常的机理; 说清 Vue 2 双端比较四指针的流程与 Vue 3 预处理加最长递增子序列的改进; 判断"索引 key 什么时候无害",避免一刀切。
本节摘要:key 是 diff 在列表比对时识别"同一个节点"的标识。用稳定业务 id 与用数组索引,会在插入、删除场景产生完全不同的补丁序列。本节从双端比较讲到最长递增子序列,复盘索引 key 的三个事故现场,并澄清"用 id 就一定对"的边界情况。
阅读完本节,你应当能够:
子节点是数组时,patch 面对的不是一对一而是一对多:旧列表 [A, B, C],新列表 [A, D, B, C]——D 是插入的新项,还是 B 改名换姓?没有身份信息,算法只能按位置顺序配对,得出"B 变成了 D、C 变成了 B、末尾新增 C"的错误结论:三次无谓更新,且 B、C 内部的状态(输入值、选中态)全部被张冠李戴。
key 就是给每个节点发的身份证:配对时优先按 key 匹配而不是按位置匹配,于是 D 被正确识别为新增,B、C 原样复用,一次插入搞定。
Vue 2:双端比较。 新旧列表各维护头尾两个指针,共四种头尾组合先比,命中就复用并移动指针;四轮不中再按 key 建索引映射查找。对"头部插入、尾部追加"这类常见操作,双端都能秒配,性能稳定。
Vue 3:预处理 + 最长递增子序列。 先同步比头部与尾部(处理常见的前插后加),剩余中间段按 key 建立映射,找出新序列中相对顺序不变的最长节点序列(最长递增子序列),这些节点原地不动,其余节点才移动。数学上保证了移动次数最少。
不需要会写子序列算法,记住结论即可:Vue 3 的列表更新移动次数已接近理论最优,你写 key 与否才是决定配对质量的根本变量。
现场一:输入内容串位。 列表每项带输入框:
<li v-for="(item, index) in items" :key="index"> <input type="text"> {{ item.name }} </li>
在头部插入一项后,index 0 还是 0,diff 按 key 配对认为"位置 0 还是原来那个节点",于是复用旧节点——但旧位置 0 的输入框里存的是用户已经敲进去的内容。用户看到的效果:明明插入了新行,第一行输入框却留着上一行的字。改成 :key="item.id",配对按业务身份走,新项新建节点,输入框干干净净。
现场二:组件状态错挂。 列表项是视频播放组件,删除第一项后按索引复用,第二项的播放进度被安到了第一项头上。凡是列表项内部有状态的场景(播放器、表单、折叠面板),索引 key 都是一颗定时雷。
现场三:过渡动画错乱。 使用 transition-group 做列表动画时,索引 key 让移动动画从"项滑到新位置"退化成"内容原地闪变",因为框架认为节点没动、只是内容变了。

一刀切禁止索引 key 也没必要。满足以下全部条件时,索引 key 与 id key 行为等价:列表只做整体替换、从不在头尾或中间增删、不重排、列表项是纯展示组件。典型如分页表格整页刷数据。但项目演化后这些前提很容易被打破(某天产品要求支持拖拽排序),我倾向于一开始就用 id——成本为零,免疫未来变更。
反过来,"用 id 就一定对"也有边界:id 必须稳定且唯一。用随机数当 key(每次渲染重新生成)比索引更糟,等于强制全量重建;两个列表项 id 撞车时,Vue 3 会直接警告并可能引发复用混乱。数据没有天然 id 时,在数据创建时生成一次并随数据持久化,而不是渲染时现场编。
v-for 的 key 不是给 CSS 用的。 偶尔看到有人用 key 强制刷新某个子树(改 key 触发重建),这是可行但昂贵的逃生门——等同于放弃复用,组件状态全部丢失。它的合理使用场景近乎只有"确实需要彻底重置"的场合。
Vue 3 的 key 在分支上也生效。 v-if / v-else 分支若标签相同,也可以用 key 区分,明确告诉 diff"这是两个不同节点,别复用",避免分支切换时残留状态。这是 key 语义从列表向条件渲染的自然延伸。
到这里,渲染管线三站全部打通。下一章看管线如何被封装成可复用的单元——组件。