key:列表更新的身份证
🎯 引言
列表是页面中出现频率最高的结构。这篇我们通过两个对比实验,看清 key 在列表更新中的决定性作用——它直接决定了 Snabbdom 是"聪明地搬家"还是"笨拙地改写"。
🧱 实验一:没有 key 的列表更新
先渲染一个水果列表:
import { init, h } from 'snabbdom';
const patch = init([]);
// 渲染列表的函数
function renderList(list) {
return h(
'ul',
list.map((item) => h('li', item))
);
}
let list = ['苹果', '香蕉', '橙子'];
let vnode = renderList(list);
vnode = patch(document.getElementById('app'), vnode);
现在在列表头部插入"草莓":
list = ['草莓', '苹果', '香蕉', '橙子'];
vnode = patch(vnode, renderList(list));
打开开发者工具观察,Snabbdom 实际做了这些事:
| 位置 | 更新前 | 更新后 | 操作 |
|---|---|---|---|
| 第 1 个 li | 苹果 | 草莓 | 改写文字 |
| 第 2 个 li | 香蕉 | 苹果 | 改写文字 |
| 第 3 个 li | 橙子 | 香蕉 | 改写文字 |
| 第 4 个 li | — | 橙子 | 新建 |
明明只是插入了一个"草莓",前三个 li 的文字却被全部改写了一遍。因为没有 key,Snabbdom 只能按位置就地复用:第 N 个旧 li 和第 N 个新 li 选择器相同,就被当作同一个节点更新内容。
✨ 实验二:加上 key
给每个 li 加上 key(用水果名这个唯一标识):
function renderList(list) {
// key 写在 data 里
return h(
'ul',
list.map((item) => h('li', { key: item }, item))
);
}
同样做头部插入,这次 Snabbdom 的操作是:
| 操作 | 数量 |
|---|---|
| 新建"草莓"元素插入头部 | 1 次 |
| 改写"苹果""香蕉""橙子"的文字 | 0 次 |
有了 key,Snabbdom 能认出"这个 li 还是原来那颗苹果",于是把真实元素原样保留,只创建了一个新 li。从"3 次改写 + 1 次新建",变成只有"1 次新建"。
key 就像节点的身份证:有了它,无论节点在列表里怎么移动,Snabbdom 都能精准认出"你是谁",从而把 DOM 操作降到最少。
⚖️ 两种策略对比
| 对比项 | 无 key | 有 key |
|---|---|---|
| 识别方式 | 按位置就地复用 | 按 key 精准匹配 |
| 头部插入 | 后续元素全部改写文字 | 只新建一个元素 |
| 元素状态(如输入框内容) | 容易错位、串数据 | 跟随元素移动,不串 |
| 适用场景 | 静态、不变动的简单列表 | 会增删、排序的动态列表 |
第三行尤其要警惕:如果 li 里有输入框,用户在第 2 个输入框输了内容,头部插入一条数据后,无 key 时输入内容会留在原位置(因为元素按位置复用),看起来就像"数据串行了"。
🪤 为什么不能用 index 作 key
既然 key 要唯一,用数组下标 index 行不行?
// ❌ 不推荐
list.map((item, index) => h('li', { key: index }, item));
头部插入"草莓"后,下标全部重新编号:原来的 0(苹果) 变成 1,1(香蕉) 变成 2……key 和数据的对应关系全乱了,Snabbdom 依然会按新下标就地改写文字——等于没加 key。
正确做法:用数据自身的唯一标识,比如后端返回的 id:
list.map((item) => h('li', { key: item.id }, item.name));
:key 的要求与这里完全一致。Vue2 的列表 Diff 正是基于 Snabbdom 实现的,你在这里学到的每一条规则,都适用于 Vue。🧾 小节总结
- key 写在虚拟节点的
data里:h('li', { key: 唯一值 }, text)。 - 无 key 时列表按位置就地复用,头部插入会导致后续元素全部改写。
- 有 key 时按 key 精准匹配,只新建或移动必要的元素。
- key 必须唯一且稳定,不要用数组下标
index。 - 会增删、排序的动态列表一定要加 key;纯静态列表可以不加。
❓ 知识问答
Q1:key 会渲染到 HTML 上吗?
A:不会。key 只存在于虚拟节点对象上,供 Diff 算法识别使用,真实 DOM 中看不到它。
Q2:兄弟节点的 key 可以重复吗?
A:绝对不行。key 重复会让 Snabbdom 无法区分节点,更新结果不可预期。同一层级内 key 必须唯一。
Q3:字符串 '1' 和数字 1 是同一个 key 吗?
A:不是。Snabbdom 用严格相等(===)比较 key,'1' 和 1 会被当成两个不同的 key。请保证 key 的类型一致。
Q4:只在尾部追加、从不乱序的列表,需要 key 吗?
A:尾部追加时不加 key 影响也不大(新旧元素按位置正好对上)。但只要列表存在插入、删除、排序的可能,就建议加上 key。
🧪 小练习
渲染一个待办列表(每条包含 id 和 title),点击"置顶第二条"按钮时,把第二条数据移到最前面。要求用 key 渲染,并在开发者工具中观察:只有元素位置移动,没有文字改写。
import { init, h } from 'snabbdom';
const patch = init([]);
const todos = [
{ id: 1, title: '学习 h 函数' },
{ id: 2, title: '学习模块' },
{ id: 3, title: '学习 key' }
];
// 请在这里编写代码
🎉 恭喜你已经理解了 key 的作用!下一篇我们深入 Diff 原理,看看 Snabbdom 对比两棵树时的完整策略。
