React 渲染循环不要用 indexOf 和 includes
jsx function TodoList({ todos, selectedIds }) { return (
-
{todos.map((todo) => {
// ❌ 错误做法:在循环内部每次都遍历 selectedIds
const isSelected = selectedIds.includes(todo.id);
return (
<li key={todo.id} className={isSelected ? 'selected' : ''}>
{todo.text}
);
})}
或者使用 `indexOf`:
```jsx
const isSelected = selectedIds.indexOf(todo.id) !== -1;
在小型项目中,这种写法可能不会产生明显问题。但当 todos 和 selectedIds 的长度增大时,性能会急剧下降。
为什么它们是有害的
时间复杂度导致“二次方”爆炸
Array.prototype.includes()和Array.prototype.indexOf()的时间复杂度都是 O(n)(线性查找)。- 在
map循环中,对每一个todo都调用一次includes,意味着整体时间复杂度变成了 O(n × m)(n 为列表长度,m 为检查数组长度)。当 n 与 m 都很大的时候(比如各有一万项),就会执行一亿次比较!
React 的每次渲染都会重新执行 map 内的函数,如果父组件频繁重绘,这些昂贵的计算就会被反复触发,导致界面卡顿、丢帧。即使你用 React.memo 包裹了列表项,但因为传递给子组件的 isSelected 在每次渲染时都是新计算出的布尔值(引用可能不变,但计算过程已经执行),性能损耗已经发生了。
浪费 JavaScript 引擎的优化机会
现代 JS 引擎对重复的线性查找无法进行有效优化。相反,如果将判断数据结构换成 Set 或 Map,查找复杂度为 O(1),即使数据量增长,性能也几乎不变。
可能隐藏的 Bug(索引作为 key 的相关问题)
尽管题目重点不在 key,但 includes 和 indexOf 常常被错误地与“使用索引作为 key”结合使用。例如:
{todos.map((todo, index) => {
const isSelected = selectedIndices.includes(index); // 使用索引作为判断依据
...
})}
当列表顺序改变、增加或删除项目时,由于索引不稳定,会导致状态错乱、动画异常或输入框内容跳位等严重 bug。因此,不仅应避免使用 indexOf/includes 进行性能敏感的查找,还应始终使用稳定且唯一的标识符作为 key(如 todo.id)。
正确做法:预处理为 Set 或 Map
在进入渲染循环之前,将需要进行查找的数组转化为 Set,然后利用 Set.prototype.has() 进行 O(1) 复杂度的判断。
基础版本
function TodoList({ todos, selectedIds }) {
// ✅ 一次转换,O(n) 预处理
const selectedSet = new Set(selectedIds);
return (
<ul>
{todos.map((todo) => {
// ✅ O(1) 查找
const isSelected = selectedSet.has(todo.id);
return (
<li key={todo.id} className={isSelected ? 'selected' : ''}>
{todo.text}
</li>
);
})}
</ul>
);
}
这样整体复杂度降为 O(n + m),即便数据规模上万也能流畅运行。
与 React 性能优化结合:useMemo
如果 selectedIds 在多次渲染中引用不变(但 React 可能会因父组件重绘而重新传入一个新数组,即使内容相同),每次渲染都重新 new Set(selectedIds) 仍然会产生新的 Set 对象,但计算量并不大(O(m))。如果你希望极致优化,可以用 useMemo 将 Set 缓存起来:
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
只有当 selectedIds 数组引用发生变化时才会重新创建 Set。注意:这里的依赖项是数组本身,如果父组件每次渲染都生成全新的数组(内容相同),则 useMemo 仍会重新创建 Set,这是合理的,因为我们需要的是 Set 的内容正确。但你可以通过让父组件保持引用稳定(使用 useMemo 或 useState 避免不必要的重创建)来进一步减少浪费。
处理更复杂的结构:使用 Map
如果你需要根据 ID 获取某个完整对象,而不是简单的布尔值,可使用 Map:
const selectedMap = useMemo(() => {
const map = new Map();
selectedItems.forEach(item => map.set(item.id, item));
return map;
}, [selectedItems]);
// 渲染中
const selectedItem = selectedMap.get(todo.id);
这样你既获得了 O(1) 的查找速度,又可以拿到完整的对象数据。
特殊案例:条件恒定不变时
如果查找依据的数据在组件生命周期内永远不会改变(例如一个固定的权限列表),你可以将 Set 提取到组件外部(模块级常量),避免重复创建:
const ADMINS = new Set(['admin', 'superadmin']);
function UserList({ users }) {
return users.map(user => (
<div key={user.id}>
{ADMINS.has(user.role) && <Badge>Admin</Badge>}
</div>
));
}