React 渲染循环不要用 indexOf 和 includes

FreeGuideOnline 最新 2026-07-04

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;

在小型项目中,这种写法可能不会产生明显问题。但当 todosselectedIds 的长度增大时,性能会急剧下降。

为什么它们是有害的

时间复杂度导致“二次方”爆炸

  • 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 引擎对重复的线性查找无法进行有效优化。相反,如果将判断数据结构换成 SetMap,查找复杂度为 O(1),即使数据量增长,性能也几乎不变。

可能隐藏的 Bug(索引作为 key 的相关问题)

尽管题目重点不在 key,但 includesindexOf 常常被错误地与“使用索引作为 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))。如果你希望极致优化,可以用 useMemoSet 缓存起来:

const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);

只有当 selectedIds 数组引用发生变化时才会重新创建 Set。注意:这里的依赖项是数组本身,如果父组件每次渲染都生成全新的数组(内容相同),则 useMemo 仍会重新创建 Set,这是合理的,因为我们需要的是 Set 的内容正确。但你可以通过让父组件保持引用稳定(使用 useMemouseState 避免不必要的重创建)来进一步减少浪费。

处理更复杂的结构:使用 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>
  ));
}