返回首页

说说对 React 的 keys 的理解?应用场景?

问题解析(面试官考察点)

面试官通过此问题主要考察:

  • 对 key 属性的作用和重要性的理解
  • 对 React Diff 算法中 key 的作用的理解
  • 对 key 使用注意事项的了解
  • 对列表渲染性能优化的认识

核心概念(基础知识点)

什么是 Key

Key 是 React 元素的一个特殊属性,用于帮助 React 识别哪些元素改变了(添加、删除、修改)。Key 在兄弟元素之间应该是唯一的,但不需要全局唯一。

Key 的核心作用

  1. 身份标识: 帮助 React 识别数组中的每个元素
  2. Diff 优化: 提高 Diff 算法的效率,减少不必要的 DOM 操作
  3. 状态保持: 确保组件状态与正确的元素关联

详细解答(代码示例)

Key 的基本使用

// 列表渲染中使用 key
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        // 使用数据的唯一标识作为 key
        <li key={todo.id}>
          <input type="checkbox" checked={todo.completed} />
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

// 不好的做法:使用索引作为 key
function BadExample({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        // 仅在列表不会变化时使用索引
        <li key={index}>{item.name}</li>
      ))}
    </ul>
  );
}

Key 的重要性示例

// 演示 key 对状态保持的影响
class TodoItem extends React.Component {
  state = { editing: false };

  toggleEdit = () => {
    this.setState({ editing: !this.state.editing });
  };

  render() {
    const { todo } = this.props;
    return (
      <li>
        {this.state.editing ? (
          <input defaultValue={todo.text} />
        ) : (
          <span>{todo.text}</span>
        )}
        <button onClick={this.toggleEdit}>
          {this.state.editing ? "保存" : "编辑"}
        </button>
      </li>
    );
  }
}

// 使用索引作为 key - 有问题
function BadTodoList({ todos }) {
  return (
    <ul>
      {todos.map((todo, index) => (
        <TodoItem key={index} todo={todo} />
      ))}
    </ul>
  );
}
// 问题:删除第一个元素后,所有元素的状态会错乱

// 使用 id 作为 key - 正确
function GoodTodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem key={todo.id} todo={todo} />
      ))}
    </ul>
  );
}
// 正确:删除操作只会影响对应的元素

Key 在表单中的影响

// 演示 key 对表单状态的影响
function FormDemo() {
  const [users, setUsers] = useState([
    { id: 1, name: "张三" },
    { id: 2, name: "李四" },
    { id: 3, name: "王五" }
  ]);

  const removeUser = (id) => {
    setUsers(users.filter(user => user.id !== id));
  };

  return (
    <div>
      <h3>使用索引作为 key(有问题)</h3>
      {users.map((user, index) => (
        <div key={index}>
          <input defaultValue={user.name} />
          <button onClick={() => removeUser(user.id)}>删除</button>
        </div>
      ))}

      <h3>使用 id 作为 key(正确)</h3>
      {users.map(user => (
        <div key={user.id}>
          <input defaultValue={user.name} />
          <button onClick={() => removeUser(user.id)}>删除</button>
        </div>
      ))}
    </div>
  );
}

使用索引作为 Key 的场景

// 可以安全使用索引作为 key 的场景

// 1. 列表是静态的,不会增删改
function StaticList({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item}</li>
      ))}
    </ul>
  );
}

// 2. 列表不会被重新排序或筛选
function SimpleList({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{item.name}</li>
      ))}
    </ul>
  );
}

// 3. 列表项没有状态或不受控输入
function StatelessList({ items }) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>
          {/* 纯展示,没有内部状态 */}
          <span>{item.name}</span>
          <span>{item.value}</span>
        </li>
      ))}
    </ul>
  );
}

深入理解(原理剖析)

Key 与 Diff 算法

┌─────────────────────────────────────────────────────────────┐
│                   Key 在 Diff 算法中的作用                   │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  旧列表: [A, B, C, D]                                       │
│  新列表: [A, C, B, D]                                       │
│                                                              │
│  不使用 key 时的 diff:                                       │
│  - A === A (保留)                                           │
│  - B !== C (更新)                                           │
│  - C !== B (更新)                                           │
│  - D === D (保留)                                           │
│  结果:2 次更新操作                                          │
│                                                              │
│  使用 key 时的 diff:                                         │
│  - A (key=a) === A (key=a) (保留)                          │
│  - B (key=b) 移动到位置 2                                   │
│  - C (key=c) 移动到位置 1                                   │
│  - D (key=d) === D (key=d) (保留)                          │
│  结果:2 次移动操作(更高效)                                │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Diff 算法中的 Key 匹配

// React Diff 算法简化示意

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  let resultingFirstChild = null;
  let previousNewFiber = null;
  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;
  let newIdx = 0;

  // 第一轮:按位置对比,key 相同则复用
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) {
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      nextOldFiber = oldFiber.sibling;
    }

    const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);

    if (newFiber === null) {
      // key 不同,跳出循环
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      break;
    }

    // 复用或创建新 fiber
    // ...
  }

  // 第二轮:处理剩余的新元素或旧元素
  // ...

  // 第三轮:处理移动(基于 key 的 map)
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx]
    );

    if (newFiber !== null) {
      // 根据 lastPlacedIndex 判断是否需要移动
      if (newFiber.index < lastPlacedIndex) {
        // 需要移动
        newFiber.flags |= Placement;
      } else {
        lastPlacedIndex = newFiber.index;
      }
    }
  }

  return resultingFirstChild;
}

Key 对组件生命周期的影响

// 演示 key 如何影响组件的挂载和卸载
class Counter extends React.Component {
  state = { count: 0 };

  componentDidMount() {
    console.log(`Counter ${this.props.id} mounted`);
  }

  componentWillUnmount() {
    console.log(`Counter ${this.props.id} will unmount`);
  }

  componentDidUpdate() {
    console.log(`Counter ${this.props.id} updated`);
  }

  increment = () => {
    this.setState({ count: this.state.count + 1 });
  };

  render() {
    return (
      <div>
        <span>Counter {this.props.id}: {this.state.count}</span>
        <button onClick={this.increment}>+1</button>
      </div>
    );
  }
}

function App() {
  const [items, setItems] = useState([
    { id: 1 },
    { id: 2 },
    { id: 3 }
  ]);

  const reverseItems = () => {
    setItems([...items].reverse());
  };

  return (
    <div>
      <h3>使用 id 作为 key(组件会移动,不会重新挂载)</h3>
      {items.map(item => (
        <Counter key={item.id} id={item.id} />
      ))}

      <h3>使用索引作为 key(组件会重新挂载)</h3>
      {items.map((item, index) => (
        <Counter key={index} id={item.id} />
      ))}

      <button onClick={reverseItems}>反转列表</button>
    </div>
  );
}

最佳实践

1. 选择合适的 Key

// 推荐:使用数据的唯一标识
function UserList({ users }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

// 如果没有唯一标识,考虑生成唯一 ID
import { v4 as uuidv4 } from "uuid";

function ItemList({ items }) {
  // 为每个 item 添加稳定 id
  const itemsWithId = useMemo(() => {
    return items.map(item => ({
      ...item,
      _id: item.id || uuidv4()
    }));
  }, [items]);

  return (
    <ul>
      {itemsWithId.map(item => (
        <li key={item._id}>{item.name}</li>
      ))}
    </ul>
  );
}

// 避免:使用随机数或时间戳作为 key
function BadExample({ items }) {
  return (
    <ul>
      {items.map(item => (
        // 每次渲染都会生成新 key,导致组件重新挂载
        <li key={Math.random()}>{item.name}</li>
      ))}
    </ul>
  );
}

2. 列表排序和筛选

function SortableList({ items }) {
  const [sortOrder, setSortOrder] = useState("asc");

  const sortedItems = useMemo(() => {
    return [...items].sort((a, b) => {
      if (sortOrder === "asc") {
        return a.name.localeCompare(b.name);
      }
      return b.name.localeCompare(a.name);
    });
  }, [items, sortOrder]);

  return (
    <div>
      <button onClick={() => setSortOrder("asc")}>升序</button>
      <button onClick={() => setSortOrder("desc")}>降序</button>
      <ul>
        {sortedItems.map(item => (
          // 使用稳定的 id,排序时组件会移动而不是重新挂载
          <li key={item.id}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

3. 嵌套列表的 Key

function NestedList({ categories }) {
  return (
    <div>
      {categories.map(category => (
        // 父级 key
        <div key={category.id}>
          <h3>{category.name}</h3>
          <ul>
            {category.items.map(item => (
              // 子级 key,只需要在兄弟元素中唯一
              <li key={item.id}>{item.name}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}

4. 使用 Key 重置组件状态

// 利用 key 重置组件状态
function ResettableForm() {
  const [formKey, setFormKey] = useState(0);

  const resetForm = () => {
    // 改变 key 会导致组件重新挂载,状态重置
    setFormKey(prev => prev + 1);
  };

  return (
    <div>
      <UserForm key={formKey} />
      <button onClick={resetForm}>重置表单</button>
    </div>
  );
}

function UserForm() {
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  return (
    <form>
      <input
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="姓名"
      />
      <input
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="邮箱"
      />
    </form>
  );
}

面试要点

  1. Key 的作用: 帮助 React 识别元素,优化 Diff 算法,保持组件状态正确

  2. 使用索引作为 key 的问题:

    • 列表增删改时会导致状态错乱
    • 性能问题(不必要的重新渲染)
    • 仅在静态列表中可以使用
  3. 最佳实践:

    • 使用数据的唯一标识(如 id)作为 key
    • 避免使用随机数或索引作为 key
    • 确保 key 在兄弟元素中唯一且稳定
  4. Key 的特殊用途:

    • 可以强制组件重新挂载(改变 key)
    • 影响组件的生命周期(mount/unmount)
  5. Diff 算法: Key 帮助 React 识别哪些元素可以复用,减少 DOM 操作