MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 使用 Keys 管理列表项

2023-01-131.5k 阅读

为什么 React 需要 Keys 来管理列表项

在 React 开发中,当我们处理列表数据并将其渲染到页面上时,会遇到一个关键问题:React 如何高效地跟踪列表中每个元素的状态和变化,以便在数据更新时能够准确且高效地更新 DOM 呢?这就是 keys 发挥作用的地方。

React 的核心机制之一是虚拟 DOM(Virtual DOM)。当组件的状态或属性发生变化时,React 会创建一个新的虚拟 DOM 树,并与之前的虚拟 DOM 树进行对比,通过计算差异(称为 “diffing” 算法),找出真正需要更新的 DOM 部分,然后只对这些部分进行实际的 DOM 更新,从而提高性能。

在处理列表时,如果没有 keys,React 就难以准确判断列表中的哪一项发生了变化。例如,假设我们有一个简单的待办事项列表,当我们添加或删除一个待办事项时,React 无法确定是哪一个具体的项发生了改变,它可能会错误地重新渲染整个列表,而不是只更新受影响的部分。这不仅效率低下,还可能导致一些意外的行为,比如输入框失去焦点、动画效果异常等。

通过为列表中的每一项指定一个唯一的 key,React 就能够精确地识别每一个列表项。key 就像是列表项的 “身份证”,使得 React 在进行虚拟 DOM 对比时,能够准确地判断哪些项是新增的、哪些项被删除了、哪些项的位置发生了变化以及哪些项的内容改变了。这样,React 就能更高效地更新 DOM,只对真正需要改变的部分进行操作,提升应用的性能。

Keys 的基本使用

在 React 中使用 keys 非常简单。当我们渲染一个列表时,只需要为数组中的每一个元素添加一个 key 属性。例如,我们有一个包含人员姓名的数组,想要将其渲染为一个无序列表:

import React from 'react';

const names = ['Alice', 'Bob', 'Charlie'];

function NameList() {
  return (
    <ul>
      {names.map((name, index) => (
        <li key={index}>{name}</li>
      ))}
    </ul>
  );
}

export default NameList;

在上述代码中,我们使用 map 方法遍历 names 数组,并为每个 <li> 元素添加了一个 key 属性,这里我们使用了数组的索引 index 作为 key。虽然使用索引作为 key 在某些简单场景下可以工作,但并不推荐在大多数情况下使用,我们稍后会详细说明原因。

通常,我们应该使用列表项中一个稳定且唯一的标识符作为 key。例如,如果我们的人员数据包含一个 id 属性,就应该使用 id 作为 key

import React from 'react';

const people = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' }
];

function PeopleList() {
  return (
    <ul>
      {people.map((person) => (
        <li key={person.id}>{person.name}</li>
      ))}
    </ul>
  );
}

export default PeopleList;

这样,React 就可以通过 id 准确地识别每个列表项,即使列表的顺序发生变化或者某些项被添加、删除,React 也能正确地更新 DOM。

使用索引作为 Keys 的问题

虽然使用索引作为 key 看起来很方便,但在很多实际场景中会带来问题。主要问题出现在列表的顺序发生变化或者列表项被添加、删除时。

假设我们有一个任务列表,任务对象包含 idtask 属性:

import React, { useState } from'react';

const tasks = [
  { id: 1, task: 'Learn React' },
  { id: 2, task: 'Build a project' },
  { id: 3, task: 'Deploy the app' }
];

function TaskList() {
  const [taskList, setTaskList] = useState(tasks);

  const handleDelete = (taskId) => {
    const newList = taskList.filter(task => task.id!== taskId);
    setTaskList(newList);
  };

  return (
    <ul>
      {taskList.map((task, index) => (
        <li key={index}>
          {task.task}
          <button onClick={() => handleDelete(task.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

export default TaskList;

在这个例子中,如果我们使用索引作为 key,当用户点击 “Delete” 按钮删除一个任务时,就会出现问题。假设用户删除了 “Build a project” 任务(id 为 2),那么原来 id 为 3 的任务在数组中的索引就会从 2 变为 1。React 会根据 key(即索引)来判断列表项的变化,它会认为索引为 1 的项内容发生了改变,而不是索引为 2 的项被删除了。这可能会导致一些意外的行为,比如输入框失去焦点(如果列表项中包含输入框),或者一些依赖于项的稳定标识的功能出现错误。

另外,当我们对列表进行排序时,使用索引作为 key 也会引发类似的问题。React 会错误地认为所有项的内容都发生了改变,从而重新渲染所有项,而不是仅仅更新它们的顺序。

正确选择 Keys

为了避免使用索引作为 key 带来的问题,我们应该始终选择列表项中稳定且唯一的标识符作为 key。通常,数据本身会包含这样的标识符,比如数据库中的 id。如果数据本身没有合适的标识符,我们可以在生成数据时手动添加一个唯一标识。

例如,假设我们正在开发一个笔记应用,笔记数据没有自带的 id,我们可以在创建笔记时为其生成一个唯一的 id

import React, { useState } from'react';

function generateUniqueId() {
  return Math.random().toString(36).substr(2, 9);
}

const initialNotes = [
  { content: 'First note' },
  { content: 'Second note' }
];

const newNotes = initialNotes.map(note => ({...note, id: generateUniqueId() }));

function NoteList() {
  const [notes, setNotes] = useState(newNotes);

  const handleAddNote = () => {
    const newNote = { content: 'New note', id: generateUniqueId() };
    setNotes([...notes, newNote]);
  };

  return (
    <div>
      <button onClick={handleAddNote}>Add Note</button>
      <ul>
        {notes.map(note => (
          <li key={note.id}>{note.content}</li>
        ))}
      </ul>
    </div>
  );
}

export default NoteList;

在这个例子中,我们通过 generateUniqueId 函数为每个笔记生成一个唯一的 id,并在渲染列表时使用这个 id 作为 key。这样,无论我们添加、删除还是重新排序笔记,React 都能准确地识别每个笔记项,确保应用的正确行为和高效性能。

Keys 在嵌套列表中的应用

当我们处理嵌套列表时,同样需要为每一层的列表项指定 key。例如,假设我们有一个包含部门和员工的组织结构数据,需要将其渲染为一个树形结构:

import React from'react';

const organization = [
  {
    id: 1,
    department: 'Engineering',
    employees: [
      { id: 11, name: 'Alice' },
      { id: 12, name: 'Bob' }
    ]
  },
  {
    id: 2,
    department: 'Marketing',
    employees: [
      { id: 21, name: 'Charlie' },
      { id: 22, name: 'David' }
    ]
  }
];

function OrganizationList() {
  return (
    <ul>
      {organization.map(department => (
        <li key={department.id}>
          {department.department}
          <ul>
            {department.employees.map(employee => (
              <li key={employee.id}>{employee.name}</li>
            ))}
          </ul>
        </li>
      ))}
    </ul>
  );
}

export default OrganizationList;

在上述代码中,我们为外层的部门列表项使用部门的 id 作为 key,为内层的员工列表项使用员工的 id 作为 key。这样,React 就能正确地处理嵌套列表的更新,无论是部门的添加、删除,还是员工的变动。

Keys 与 React 组件复用

keys 不仅对 React 更新 DOM 有帮助,还在组件复用方面起着重要作用。当一个组件作为列表项被渲染时,key 可以帮助 React 决定是否复用已有的组件实例,还是创建一个新的实例。

例如,我们有一个简单的计数器组件,作为列表项渲染:

import React, { useState } from'react';

function Counter() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

const counterList = [1, 2, 3];

function CounterList() {
  return (
    <div>
      {counterList.map((item) => (
        <Counter key={item} />
      ))}
    </div>
  );
}

export default CounterList;

在这个例子中,每个 Counter 组件都有自己独立的状态。如果没有 key,当列表的顺序发生变化或者某些项被添加、删除时,React 可能无法正确复用 Counter 组件实例,导致状态丢失或错误。通过为每个 Counter 组件指定 key,React 可以根据 key 来判断是否复用已有的组件实例,确保每个组件的状态能够正确地保持和更新。

Keys 的作用域和唯一性

key 的唯一性是相对于其兄弟节点而言的。也就是说,在同一级别的列表中,每个列表项的 key 必须是唯一的,但不同级别的列表可以使用相同的 key 值。

例如,在一个多层嵌套的列表中:

import React from'react';

const outerList = [
  {
    id: 1,
    innerList: [
      { subId: 11, value: 'A' },
      { subId: 12, value: 'B' }
    ]
  },
  {
    id: 2,
    innerList: [
      { subId: 21, value: 'C' },
      { subId: 22, value: 'D' }
    ]
  }
];

function OuterList() {
  return (
    <ul>
      {outerList.map(outerItem => (
        <li key={outerItem.id}>
          Outer Item {outerItem.id}
          <ul>
            {outerItem.innerList.map(innerItem => (
              <li key={innerItem.subId}>{innerItem.value}</li>
            ))}
          </ul>
        </li>
      ))}
    </ul>
  );
}

export default OuterList;

在这里,外层列表项使用 id 作为 key,内层列表项使用 subId 作为 key。虽然不同外层列表中的内层列表项可能有相同的 subId,但由于它们处于不同的作用域(不同的外层列表项内部),所以这是允许的。

Keys 与 React Fragments

React Fragments 是一种在不额外创建 DOM 节点的情况下分组多个元素的方式。当我们在 Fragments 中渲染列表时,同样需要为列表项指定 key

例如:

import React from'react';

const items = [1, 2, 3];

function ItemList() {
  return (
    <>
      {items.map(item => (
        <React.Fragment key={item}>
          <p>Item {item}</p>
          <hr />
        </React.Fragment>
      ))}
    </>
  );
}

export default ItemList;

在这个例子中,我们使用了 React Fragments 来包裹每个列表项的多个元素,并为每个 React.Fragment 指定了 key。这样,React 就能正确地处理列表项的更新,即使它们被包裹在 Fragments 中。

总结 Keys 在 React 列表管理中的重要性

在 React 前端开发中,keys 是管理列表项的关键机制。它不仅对于 React 的虚拟 DOM 对比算法至关重要,能够确保高效准确地更新 DOM,还在组件复用、保持组件状态等方面起着不可或缺的作用。

通过选择合适的、稳定且唯一的 key,我们可以避免许多因列表更新而引发的问题,保证应用的性能和稳定性。无论是简单的一维列表,还是复杂的嵌套列表,正确使用 keys 都是构建高质量 React 应用的重要环节。在实际开发中,我们应该养成为列表项指定合适 key 的习惯,以提升应用的用户体验和开发效率。

希望通过本文的详细介绍和丰富的代码示例,你对 React 中使用 keys 管理列表项有了更深入的理解和掌握,能够在自己的项目中正确、灵活地运用这一重要概念。