键
在第一章中,我们了解了 Preact 如何使用虚拟 DOM 来计算我们的 JSX 描述的两个树之间的变化,然后将这些变化应用于 HTML DOM 以更新页面。这适用于大多数场景,但有时需要 Preact “猜测”树的形状在两次渲染之间如何变化。
Preact 的猜测很可能与我们的意图不同的最常见场景是比较列表。考虑一个简单的待办事项列表组件
export default function TodoList() {
const [todos, setTodos] = useState(['wake up', 'make bed'])
function wakeUp() {
setTodos(['make bed'])
}
return (
<div>
<ul>
{todos.map(todo => (
<li>{todo}</li>
))}
</ul>
<button onClick={wakeUp}>I'm Awake!</button>
</div>
)
}
第一次渲染此组件时,将绘制两个 <li>
列表项。单击“我醒了!”按钮后,我们的 todos
状态数组将更新为仅包含第二个项目,“整理床铺”
。
以下是 Preact 在第一次和第二次渲染中“看到”的内容
第一次渲染 | 第二次渲染 |
---|---|
|
|
发现问题了吗?虽然对我们来说很明显,第一个列表项(“起床”)已删除,但 Preact 并不了解这一点。Preact 只看到有两个项,现在只剩一个。应用此更新时,它实际上会删除第二个项(<li>整理床铺</li>
),然后将第一个项的文本从起床
更新为整理床铺
。
结果在技术上是正确的——一个文本为“整理床铺”的单个项——我们得到该结果的方式并不理想。想象一下,如果列表项有 1000 个,我们删除了第一个项:Preact 不会删除单个 <li>
,而是会更新前 999 个其他项的文本,并删除最后一个项。
列表渲染的关键
在类似于上一个示例的情况下,项的顺序会发生变化。我们需要一种方法来帮助 Preact 了解哪些项是哪些项,以便它能够检测到何时添加、删除或替换每个项。为此,我们可以为每个项添加一个 key
属性。
key
属性是给定元素的标识符。元素不会按两个树之间的顺序进行比较,而是通过查找具有相同 key
属性值的先前元素来比较具有 key
属性的元素。key
可以是任何类型的值,只要它在渲染之间“稳定”即可:同一项的重复渲染应具有完全相同的 key
属性值。
让我们为上一个示例添加键。由于我们的待办事项列表是一个不会改变的简单字符串数组,因此我们可以使用这些字符串作为键
export default function TodoList() {
const [todos, setTodos] = useState(['wake up', 'make bed'])
function wakeUp() {
setTodos(['make bed'])
}
return (
<div>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
// ^^^^^^^^^^ adding a key prop
))}
</ul>
<button onClick={wakeUp}>I'm Awake!</button>
</div>
)
}
当我们第一次渲染此新版本的 <TodoList>
组件时,将绘制两个 <li>
项。单击“我醒了!”按钮时,我们的 todos
状态数组将更新为仅包含第二个项 “整理床铺”
。
现在我们已将 key
添加到列表项,Preact 会看到以下内容
第一次渲染 | 第二次渲染 |
---|---|
|
|
这一次,Preact 可以看到第一个项已删除,因为第二个树缺少 key="wake up"
的项。它将删除第一个项,并保持第二个项不变。
不使用键的情况
开发人员在使用键时遇到的最常见的陷阱之一是意外选择在渲染之间不稳定的键。在我们的示例中,假设我们使用 map()
的索引参数作为我们的 key
值,而不是 item
字符串本身
items.map((item, index) => <li key={index}>{item}</li>
这会导致 Preact 在第一次和第二次渲染时看到以下树
第一次渲染 | 第二次渲染 |
---|---|
|
|
问题在于index
实际上并未识别我们列表中的值,它识别的是位置。以这种方式渲染实际上迫使 Preact 按顺序匹配项,如果没有键,它也会这样做。将索引键应用于具有不同类型的列表项时,甚至可能强制昂贵或损坏的输出,因为键无法匹配具有不同类型的元素。
🚙 类比时间!想象一下你把车停在代客泊车场。
当你回来取车时,你告诉代客泊车员你开的是一辆灰色 SUV。不幸的是,停放的汽车中有一半以上是灰色 SUV,你最终拿到了别人的车。下一个灰色 SUV 的车主拿错了车,依此类推。
如果你告诉代客泊车员你开的是车牌号为“PR3ACT”的灰色 SUV,那么你可以确保自己的车会被归还。
作为一般经验法则,切勿将数组或循环索引用作key
。使用列表项值本身,或为项生成唯一 ID 并使用它
const todos = [
{ id: 1, text: 'wake up' },
{ id: 2, text: 'make bed' }
]
export default function ToDos() {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
</li>
))}
</ul>
)
}
记住:如果你真的找不到稳定的键,最好完全省略key
道具,而不是将索引用作键。
试试看!
对于本章的练习,我们将把我们学到的关于键的知识与我们从上一章中学到的副作用知识结合起来。
使用一个效果在<TodoList>
首次渲染后调用提供的getTodos()
函数。请注意,此函数返回一个 Promise,你可以通过调用.then(value => { })
来获取它的值。一旦你有了 Promise 的值,通过调用其关联的setTodos
方法将其存储在todos
useState 钩子中。
最后,更新 JSX 以将todos
中的每个项渲染为包含该 todo 项的.text
属性值的<li>
。