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

React 中 Key 的作用与重要性

2023-03-073.8k 阅读

一、理解 React 中的虚拟 DOM

在深入探讨 React 中 key 的作用之前,我们需要先对 React 的核心概念——虚拟 DOM 有一个清晰的认识。

React 使用虚拟 DOM 来提高性能。传统的前端开发模式中,每当数据发生变化,浏览器会重新渲染整个 DOM 树,这在大型应用中会导致严重的性能问题。而 React 通过创建一个轻量级的虚拟 DOM 树,当数据变化时,React 首先计算出虚拟 DOM 的变化,然后将这些变化批量应用到实际的 DOM 上。

例如,假设有如下 HTML 结构:

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
</ul>

在 React 中,会将其表示为一个虚拟 DOM 对象:

const list = (
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
  </ul>
);

这个虚拟 DOM 对象其实是一个普通的 JavaScript 对象,它包含了真实 DOM 元素的一些关键信息,如标签名、属性、子元素等。

当数据发生变化,比如我们要在列表中添加一个新的 li 元素:

const newList = (
  <ul>
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
  </ul>
);

React 会对比前后两个虚拟 DOM 树,找出差异(即新增的 <li>Item 3</li>),然后只将这个差异应用到真实 DOM 上,而不是重新渲染整个 <ul> 元素及其子元素。

二、虚拟 DOM 中的 Diff 算法

React 实现高效更新 DOM 的关键在于其 Diff 算法。Diff 算法的核心任务是对比新旧两个虚拟 DOM 树,找出变化的部分。

  1. 树的遍历策略 React 采用深度优先遍历的方式来比较两棵虚拟 DOM 树。它从根节点开始,依次比较每个节点及其子节点。

  2. 节点比较规则

  • 标签名比较:如果两个节点的标签名不同,React 会直接删除旧节点,创建新节点插入到 DOM 中。例如,从 <div> 变成 <span>,React 会销毁 <div> 及其子树,创建新的 <span> 及其子树。
  • 标签名相同且 key 相同:如果两个节点标签名相同且 key 值相同(这里先引入 key 的概念,后面会详细讲解),React 会认为这两个节点是相同的,只会更新节点的属性。比如 <div className="old"> 变为 <div className="new">,React 只会更新 className 属性。
  • 标签名相同但 key 不同:即使标签名相同,但 key 值不同,React 也会将其视为不同的节点,会删除旧节点并创建新节点。

三、Key 在 React 中的基本概念

  1. 什么是 Key key 是 React 用于在渲染列表时识别每个列表项的一个特殊属性。它应该是列表项中一个唯一的标识符,用于帮助 React 区分哪些项是新的,哪些项被修改了,哪些项被删除了。

  2. Key 的作用场景 最常见的使用场景是在 map 方法生成的列表中。例如,我们有一个数组存储了一些任务:

const tasks = ['Clean room', 'Buy groceries', 'Do laundry'];

我们要在 React 中渲染这个任务列表:

import React from 'react';

function TaskList() {
  const tasks = ['Clean room', 'Buy groceries', 'Do laundry'];
  return (
    <ul>
      {tasks.map((task, index) => (
        <li key={index}>{task}</li>
      ))}
    </ul>
  );
}

export default TaskList;

在上述代码中,我们为每个 <li> 元素添加了 key 属性,这里使用了数组的索引 index 作为 key。虽然这在简单场景下可能看起来没问题,但在一些复杂场景下可能会带来问题,后面我们会详细分析。

四、Key 的作用

  1. 高效的 DOM 更新 正如前面提到的,React 使用 Diff 算法来对比虚拟 DOM 的变化。当列表项有 key 时,Diff 算法能够更准确地识别每个列表项的变化。

假设我们有一个任务列表,最初的任务是 ['Clean room', 'Buy groceries'],渲染后的 DOM 结构如下:

<ul>
  <li>Clean room</li>
  <li>Buy groceries</li>
</ul>

然后我们添加一个新任务 'Do laundry',任务列表变为 ['Clean room', 'Buy groceries', 'Do laundry']。如果每个 <li> 元素都有唯一的 key,React 的 Diff 算法能够快速识别出新增的 'Do laundry' 任务,并只在 DOM 中添加对应的 <li> 元素。

但如果没有 key 或者使用了错误的 key(比如使用索引且列表顺序发生变化的情况),Diff 算法可能会错误地认为整个列表都需要重新渲染,导致性能下降。

  1. 保持组件状态 在 React 中,组件的状态是与组件实例相关联的。当列表项有唯一的 key 时,React 能够在列表更新时保持每个组件实例的状态。

例如,我们有一个可编辑的任务列表,每个任务项可以切换为编辑状态。每个任务项是一个独立的组件 TaskItem

import React, { useState } from'react';

function TaskItem({ task }) {
  const [isEditing, setIsEditing] = useState(false);
  return (
    <li>
      {isEditing? (
        <input type="text" defaultValue={task} />
      ) : (
        <span>{task}</span>
      )}
      <button onClick={() => setIsEditing(!isEditing)}>
        {isEditing? 'Save' : 'Edit'}
      </button>
    </li>
  );
}

function TaskList() {
  const tasks = ['Clean room', 'Buy groceries'];
  return (
    <ul>
      {tasks.map((task, index) => (
        <TaskItem key={index} task={task} />
      ))}
    </ul>
  );
}

export default TaskList;

在这个例子中,如果我们使用索引作为 key,当任务列表顺序发生变化时,React 可能会错误地销毁并重新创建 TaskItem 组件实例,导致编辑状态丢失。而如果使用唯一的 key,比如每个任务的唯一 ID,React 能够正确地识别组件实例,保持其状态。

五、错误使用 Key 的情况及后果

  1. 使用索引作为 Key 的问题
  • 列表顺序变化时:假设我们有一个任务列表 ['Clean room', 'Buy groceries'],使用索引作为 key 渲染列表。然后我们在列表开头添加一个新任务 'Do laundry',列表变为 ['Do laundry', 'Clean room', 'Buy groceries']。由于索引发生了变化,React 会认为所有的列表项都发生了变化,会销毁并重新创建所有的 <li> 元素及其对应的组件实例(如果是组件)。这不仅导致性能下降,还可能丢失组件的状态,比如前面提到的 TaskItem 组件的编辑状态。
  • 列表项删除时:如果我们删除了列表中间的一项,比如删除 'Buy groceries',后续项的索引会重新计算。这同样会让 React 认为后续项都发生了变化,进行不必要的重新渲染和组件实例的销毁与创建。
  1. 使用随机数作为 Key 的问题 有些开发者可能会为了确保唯一性,使用随机数作为 key。例如:
function TaskList() {
  const tasks = ['Clean room', 'Buy groceries'];
  return (
    <ul>
      {tasks.map((task) => (
        <li key={Math.random()}>{task}</li>
      ))}
    </ul>
  );
}

这种做法的问题在于,每次组件重新渲染,随机数都会改变。这会导致 React 认为每个列表项都是全新的,每次渲染都会销毁并重新创建所有的列表项,严重影响性能。

六、正确选择 Key

  1. 使用唯一标识符 最好的方式是使用数据本身的唯一标识符作为 key。比如,如果我们的任务数据结构包含一个唯一的 id 字段:
const tasks = [
  { id: 1, text: 'Clean room' },
  { id: 2, text: 'Buy groceries' },
  { id: 3, text: 'Do laundry' }
];

function TaskList() {
  return (
    <ul>
      {tasks.map((task) => (
        <li key={task.id}>{task.text}</li>
      ))}
    </ul>
  );
}

这样,无论列表如何变化,只要 id 不变,React 就能准确地识别每个列表项,保证高效的更新和状态保持。

  1. 生成唯一 Key 如果数据本身没有唯一标识符,可以考虑在数据生成阶段或者加载阶段为其添加唯一标识符。例如,使用 uuid 库生成唯一 ID:
import { v4 as uuidv4 } from 'uuid';

const tasks = [
  { text: 'Clean room' },
  { text: 'Buy groceries' },
  { text: 'Do laundry' }
];

const tasksWithIds = tasks.map(task => ({...task, id: uuidv4() }));

function TaskList() {
  return (
    <ul>
      {tasksWithIds.map((task) => (
        <li key={task.id}>{task.text}</li>
      ))}
    </ul>
  );
}

通过这种方式,为每个任务添加了唯一的 id,使得在 React 中能够正确地使用 key 来管理列表。

七、Key 在复杂列表场景中的应用

  1. 嵌套列表 在嵌套列表的场景中,key 的使用同样重要。例如,我们有一个部门列表,每个部门又有员工列表:
const departments = [
  {
    id: 1,
    name: 'Engineering',
    employees: [
      { id: 11, name: 'John' },
      { id: 12, name: 'Jane' }
    ]
  },
  {
    id: 2,
    name: 'Marketing',
    employees: [
      { id: 21, name: 'Bob' },
      { id: 22, name: 'Alice' }
    ]
  }
];

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

在这个例子中,外层列表(部门列表)使用部门的 id 作为 key,内层列表(员工列表)使用员工的 id 作为 key。这样 React 能够准确地管理嵌套列表的更新,当部门或者员工信息发生变化时,进行高效的 DOM 更新。

  1. 动态列表操作 在实际应用中,列表常常需要进行动态操作,如添加、删除、排序等。以一个可排序的任务列表为例:
import React, { useState } from'react';

const tasks = [
  { id: 1, text: 'Clean room' },
  { id: 2, text: 'Buy groceries' },
  { id: 3, text: 'Do laundry' }
];

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

  const handleSort = () => {
    const sortedTasks = [...taskList].sort((a, b) => a.text.localeCompare(b.text));
    setTaskList(sortedTasks);
  };

  return (
    <div>
      <button onClick={handleSort}>Sort Tasks</button>
      <ul>
        {taskList.map(task => (
          <li key={task.id}>{task.text}</li>
        ))}
      </ul>
    </div>
  );
}

export default TaskList;

在这个代码中,当点击“Sort Tasks”按钮时,任务列表会按照文本内容排序。由于使用了任务的 id 作为 key,React 能够在排序操作后准确地识别每个任务项,只更新 DOM 中受影响的部分,而不是重新渲染整个列表。

八、Key 与 React 性能优化

  1. 减少不必要的重新渲染 正确使用 key 可以显著减少不必要的重新渲染。当列表数据发生变化时,如果 key 能够准确标识每个列表项,React 的 Diff 算法就能快速定位变化的部分,只更新相关的 DOM 元素。这避免了对整个列表甚至整个组件树的重新渲染,从而提高了应用的性能。

  2. 优化内存使用 由于 key 帮助 React 准确识别组件实例,在列表更新时,React 不需要频繁地销毁和重新创建组件实例。这有助于优化内存使用,特别是在列表项是复杂组件且包含大量状态的情况下。例如,一个包含图表、表单等复杂 UI 的列表项组件,如果因为 key 使用不当导致频繁销毁和重建,会消耗大量的内存资源。而正确的 key 可以确保组件实例在需要时被复用,减少内存开销。

九、在 React 框架生态中的 Key

  1. 与 React Router 的结合 在使用 React Router 进行路由管理时,key 也发挥着重要作用。例如,当我们有多个路由组件,并且这些组件之间可能会有动态切换时,为路由组件添加合适的 key 可以确保组件状态的正确保持。

假设我们有一个博客应用,有文章列表页面和文章详情页面。文章列表页面展示多篇文章,点击文章标题进入文章详情页面。如果在路由切换过程中没有正确使用 key,可能会导致文章详情页面在返回列表页面后重新渲染,丢失用户的一些操作状态(比如滚动位置等)。

import { BrowserRouter as Router, Routes, Route } from'react-router-dom';
import ArticleList from './ArticleList';
import ArticleDetail from './ArticleDetail';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<ArticleList key="article-list" />} />
        <Route path="/article/:id" element={<ArticleDetail key="article-detail" />} />
      </Routes>
    </Router>
  );
}

export default App;

在上述代码中,为 ArticleListArticleDetail 组件添加 key,可以帮助 React Router 在路由切换时更好地管理组件状态。

  1. 与 React Native 的关系 在 React Native 开发移动应用时,key 的作用同样不可忽视。无论是列表视图(如 FlatListSectionList 等)还是其他可复用组件,使用 key 都能提高应用的性能和稳定性。

例如,在一个展示商品列表的 React Native 应用中,使用 FlatList 组件:

import React from'react';
import { FlatList, Text } from'react-native';

const products = [
  { id: 1, name: 'Product 1' },
  { id: 2, name: 'Product 2' },
  { id: 3, name: 'Product 3' }
];

const renderProduct = ({ item }) => (
  <Text key={item.id}>{item.name}</Text>
);

function ProductList() {
  return (
    <FlatList
      data={products}
      renderItem={renderProduct}
    />
  );
}

export default ProductList;

通过为每个商品项添加 keyFlatList 能够高效地管理列表的渲染和更新,在商品数据变化时,准确地进行 DOM(在 React Native 中是原生视图)的更新,提高应用的流畅性。

十、总结 Key 的重要性及最佳实践

  1. Key 的重要性总结
  • 高效更新 DOMkey 是 React Diff 算法准确识别列表项变化的关键,能够避免不必要的 DOM 重新渲染,提高性能。
  • 保持组件状态:确保在列表更新时,组件实例的状态能够正确保持,不会因为错误的识别而丢失状态。
  • 优化内存和资源使用:避免频繁销毁和重新创建组件实例,节省内存和其他资源。
  1. 最佳实践
  • 使用唯一标识符:优先使用数据本身的唯一标识符作为 key,如数据库中的 id 字段。
  • 避免使用索引和随机数:除非列表是静态的且不会发生顺序变化,否则不要使用索引作为 key,绝对不要使用随机数作为 key
  • 在复杂场景中正确应用:无论是嵌套列表还是动态列表操作,都要确保为每个列表项提供唯一且稳定的 key
  • 结合框架生态使用:在 React Router、React Native 等 React 生态系统中,同样要正确使用 key 来优化组件管理和性能。

通过深入理解和正确使用 key,开发者能够打造出性能更优、用户体验更好的 React 应用。在实际开发中,要时刻注意 key 的使用,避免因错误使用 key 而带来的性能问题和组件状态管理问题。