迭代数组时要小心!!
一个常见的误解是,使用数组中元素的索引是抑制您可能熟悉的错误的可接受方式:
Each child in an array should have a unique "key" prop.
然而,在许多情况下并非如此!这是一种反模式,在某些情况下会导致不需要的行为。
理解key
props
React 使用key
prop 来理解组件到 DOM 元素的关系,然后用于协调过程。因此,键始终保持唯一性非常重要,否则 React 很可能会混淆元素并改变不正确的元素。为了保持最佳性能,这些键在所有重新渲染过程中保持静态也很重要。
话虽如此,只要知道数组是完全静态的,并不总是需要应用上述内容。但是,鼓励尽可能应用最佳实践。
一位 React 开发人员在这个 GitHub 问题中说:
- 关键不是真的关于性能,它更多的是关于身份(这反过来会导致更好的性能)。随机分配和变化的值不是身份
- 在不知道您的数据是如何建模的情况下,我们实际上无法[自动] 提供密钥。如果您没有 id,我建议您使用某种散列函数
- 当我们使用数组时,我们已经有了内部键,但它们是数组中的索引。当您插入一个新元素时,这些键是错误的。
简而言之,akey
应该是:
- 唯一- 键不能与同级组件的键相同。
- 静态- 渲染之间不应更改键。
使用key
props
根据上面的解释,仔细研究以下示例,并在可能的情况下尝试实施推荐的方法。
坏(可能)
<tbody>
{rows.map((row, i) => {
return <ObjectRow key={i} />;
})}
</tbody>
这可以说是在 React 中迭代数组时最常见的错误。这种方法在技术上并没有“错误”,它只是……如果您不知道自己在做什么,则是“危险的”。如果您正在遍历静态数组,那么这是一种完全有效的方法(例如,导航菜单中的链接数组)。但是,如果您要添加、删除、重新排序或过滤项目,则需要小心。看看官方文档中的这个详细解释。
class MyApp extends React.Component {
constructor() {
super();
this.state = {
arr: ["Item 1"]
}
}
click = () => {
this.setState({
arr: ['Item ' + (this.state.arr.length+1)].concat(this.state.arr),
});
}
render() {
return(
<div>
<button onClick={this.click}>Add</button>
<ul>
{this.state.arr.map(
(item, i) => <Item key={i} text={"Item " + i}>{item + " "}</Item>
)}
</ul>
</div>
);
}
}
const Item = (props) => {
return (
<li>
<label>{props.children}</label>
<input value={props.text} />
</li>
);
}
ReactDOM.render(<MyApp />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>
在这个片段中,我们使用了一个非静态数组,我们不限制自己将它用作堆栈。这是一种不安全的方法(您会明白为什么)。请注意,当我们将项目添加到数组的开头(基本上是 unshift)时,每个项目的值<input>
保持不变。为什么?因为key
不唯一标识每个项目。
换句话说,起初Item 1
有key={0}
。当我们添加第二个项目时,最上面的项目变成Item 2
,然后Item 1
是第二个项目。然而,现在Item 1
有key={1}
,key={0}
不再有。相反,Item 2
现在有key={0}
!!
因此,React 认为<input>
元素没有改变,因为Item
with 键0
总是在顶部!
那么为什么这种方法只是有时不好呢?
只有在以某种方式过滤、重新排列数组或添加/删除项目时,这种方法才有风险。如果它始终是静态的,那么使用它是完全安全的。例如,["Home", "Products", "Contact us"]
可以使用此方法安全地迭代导航菜单,因为您可能永远不会添加新链接或重新排列它们。
简而言之,此时您可以安全地将索引用作key
:
- 数组是静态的,永远不会改变。
- 从不过滤数组(显示数组的子集)。
- 数组永远不会重新排序。
- 该数组用作堆栈或 LIFO(后进先出)。换句话说,添加只能在数组的末尾进行(即推送),并且只能删除最后一项(即弹出)。
如果我们在上面的代码片段中将添加的项目推送到数组的末尾,则每个现有项目的顺序将始终是正确的。
很坏
<tbody>
{rows.map((row) => {
return <ObjectRow key={Math.random()} />;
})}
</tbody>
虽然这种方法可能会保证键的唯一性,但它总是会强制 react 重新渲染列表中的每个项目,即使这不是必需的。这是一个非常糟糕的解决方案,因为它极大地影响了性能。更不用说在Math.random()
两次产生相同数字的情况下不能排除密钥冲突的可能性。
不稳定的键(如由 产生的键Math.random()
)将导致不必要地重新创建许多组件实例和 DOM 节点,这可能会导致子组件的性能下降和状态丢失。
非常好
<tbody>
{rows.map((row) => {
return <ObjectRow key={row.uniqueId} />;
})}
</tbody>
这可以说是最好的方法,因为它使用数据集中每个项目唯一的属性。例如,如果rows
包含从数据库中获取的数据,则可以使用表的主键(通常是自动递增的数字)。
选择键的最佳方法是使用一个字符串,该字符串在其兄弟项中唯一标识列表项。大多数情况下,您会使用数据中的 ID 作为键
好的
componentWillMount() {
let rows = this.props.rows.map(item => {
return {uid: SomeLibrary.generateUniqueID(), value: item};
});
}
...
<tbody>
{rows.map((row) => {
return <ObjectRow key={row.uid} />;
})}
</tbody>
这也是一个很好的方法。如果您的数据集不包含任何保证唯一性的数据(例如任意数字的数组),则可能会发生密钥冲突。在这种情况下,最好在迭代之前为数据集中的每个项目手动生成一个唯一标识符。最好在安装组件或接收数据集时(例如,来自props
或来自异步 API 调用),以便仅执行一次,而不是每次重新渲染组件时执行此操作。已经有一些库可以为您提供这样的密钥。这是一个示例:react-key-index。