Snabbdom 简介

key:列表更新的身份证

通过对比实验理解 key 的作用,掌握有 key 和无 key 时列表更新的差异,知道为什么不能用 index 作 key。

🎯 引言

列表是页面中出现频率最高的结构。这篇我们通过两个对比实验,看清 key 在列表更新中的决定性作用——它直接决定了 Snabbdom 是"聪明地搬家"还是"笨拙地改写"。


🧱 实验一:没有 key 的列表更新

先渲染一个水果列表:

main.js
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);

现在在列表头部插入"草莓":

main.js
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(用水果名这个唯一标识):

main.js
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 行不行?

main.js
// ❌ 不推荐
list.map((item, index) => h('li', { key: index }, item));

头部插入"草莓"后,下标全部重新编号:原来的 0(苹果) 变成 11(香蕉) 变成 2……key 和数据的对应关系全乱了,Snabbdom 依然会按新下标就地改写文字——等于没加 key

正确做法:用数据自身的唯一标识,比如后端返回的 id

main.js
list.map((item) => h('li', { key: item.id }, item.name));
Vue 里 :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。


🧪 小练习

渲染一个待办列表(每条包含 idtitle),点击"置顶第二条"按钮时,把第二条数据移到最前面。要求用 key 渲染,并在开发者工具中观察:只有元素位置移动,没有文字改写。

main.js
import { init, h } from 'snabbdom';

const patch = init([]);

const todos = [
    { id: 1, title: '学习 h 函数' },
    { id: 2, title: '学习模块' },
    { id: 3, title: '学习 key' }
];

// 请在这里编写代码

🎉 恭喜你已经理解了 key 的作用!下一篇我们深入 Diff 原理,看看 Snabbdom 对比两棵树时的完整策略。