React.js中的数据不可变性原则

204 阅读3分钟

1.浅比较策略

React 中的浅比较策略是用于确定何时重新渲染组件的一种机制。该策略通过比较新旧属性(props)和状态(state)的引用来决定是否需要触发组件的重新渲染。以下是关于 React 浅比较策略的详细解释:

  • 引用比较:React 不会深度递归比较属性和状态对象的内部值。相反,它只比较对象的引用。这意味着如果新旧属性或状态的引用相同,React 认为数据没有发生变化,因此不会触发重新渲染。
  • 性能优势:浅比较策略是一种性能优化手段。深度比较对象内部的所有值可能会导致性能下降,因此 React 采用了这种轻量级的比较方式,以确保在大多数情况下能够快速确定是否需要重新渲染。
  • 何时触发重新渲染:当组件的 props 或 state 发生变化时,React 会首先进行浅比较。如果新旧 props 或 state 的引用不同,React 就会认为数据发生了变化,并触发重新渲染。这将导致调用组件的 render 方法以生成新的虚拟 DOM 树,并将其与旧的虚拟 DOM 树进行比较,从而确定需要更新的部分。
  • 何时不触发重新渲染:如果新旧 props 或 state 的引用相同,React 将认为数据没有变化,从而跳过重新渲染。这是因为 React 认为在这种情况下,即使属性或状态的值发生了变化,但由于引用相同,组件的输出不会发生变化,因此没有必要重新渲染。
  • 不可变性的重要性:为了正确使用 React 的浅比较策略,通常建议采用不可变性的方式来更新属性和状态。不可变性意味着在更改属性或状态时创建一个新的对象或数组,而不是直接修改现有的对象或数组。这样可以确保新旧数据的引用不同,从而触发重新渲染。

总之,React 的浅比较策略是一种性能优化策略,通过比较属性和状态的引用来确定是否需要重新渲染组件。不可变性是与这一策略配合使用的关键,可以确保组件在数据变化时能够按预期地重新渲染。

2.shallowEqual

shallowEqual 用于比较两个对象是否相等的工具函数。在 React 生态系统中,它通常用于浅比较对象,以确定两个对象是否具有相同的值。shallowEqual 函数通常由 React 的一些内部机制和自定义性能优化中使用,以避免不必要的重新渲染。


function shallowEqual(objA, objB) {
  // 如果两个对象引用相同,则它们肯定相等
  if (objA === objB) return true;

  // 如果两个对象的键的数量不同,它们肯定不相等
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);
  if (keysA.length !== keysB.length) return false;

  // 遍历对象的所有键,比较它们的值是否相等
  for (let i = 0; i < keysA.length; i++) {
    const key = keysA[i];
    if (objA[key] !== objB[key]) return false;
  }

  return true;
}

上述代码的主要部分:

  • 首先,函数检查两个对象是否引用相同的内存地址(即 objA === objB),如果是,则它们肯定相等,直接返回 true。
  • 然后,函数获取两个对象的键数组,keysA 和 keysB。
  • 如果两个对象的键数量不同(即 keysA.length !== keysB.length),则它们肯定不相等,直接返回 false。
  • 最后,函数遍历 keysA 数组,比较对应键的值是否相等。如果有任何键的值不相等,函数将返回 false,否则返回 true。

这种浅比较的方式适用于对象的值是基本数据类型(如数字、字符串、布尔值等)的情况。如果对象的值包含了引用类型,例如嵌套对象或数组,那么浅比较将只比较它们的引用,而不会深入比较它们的内部内容。

在 React 中,shallowEqual 通常用于 shouldComponentUpdate 方法、React.memo 和 useMemo 等场景,以避免不必要的组件重新渲染。这有助于提高 React 应用的性能,因为它可以防止在父组件状态变化时触发不必要的子组件重新渲染。

3.案例

代码实现了一个简单的图书列表应用,可以添加新的书籍。从 React 库中导入了 PureComponent,这是一个继承自 Component 的类,它默认实现了 shouldComponentUpdate 方法,使用浅比较策略来进行性能优化。

在 addNewBook 方法中,准备了一个新的书籍对象 newBook,然后有两种方法来添加这本书到 books 数组中:

  • 第一种方法(被注释掉的部分)是直接修改原有的 this.state.books 数组,然后再通过 setState 重新设置 books 数组。这种方法违反了数据不可变性的原则,因为直接在原数组上进行了修改,可能会引发 React 的浅比较策略失效。
  • 第二种方法是遵循不变性原则,你首先创建了一个新的数组 books,将旧的 this.state.books 数组的内容通过扩展运算符复制到新数组中,然后将新书 newBook 添加到新数组末尾。最后,你使用 setState 来更新 books 数组,确保在数据更新时创建新的数组引用,触发 React 的浅比较策略。

因此,当下列案例,采用第一种方法进行添加书籍时,点击按钮并没有新增书籍。因为违反了数据不可变性原则,没有触发PureComponent中的浅比较策略。

而采用第二种方法时,由于新旧state的引用发生变化,可以触发浅比较策略。

通过使用不变性原则,可以确保每次数据更新都会创建新的对象或数组引用,从而与 React 的浅比较策略配合,确保组件在数据变化时能够正确地重新渲染。这有助于提高应用的性能和稳定性。


import React, { PureComponent } from "react";

export class App extends PureComponent {
  constructor() {
    super();
    this.state = {
      books: [
        { name: "JS高级设计", price: 99, count: 1 },
        { name: "React高级设计", price: 78, count: 1 },
        { name: "Node高级设计", price: 82, count: 1 },
      ],
    };
  }
  addNewBook() {
    const newBook = { name: "TypeScript学习手册", price: 70, count: 1 };
    // 1.直接修改原有state,重新设置一遍
    // this.state.books.push(newBook);
    // this.setState({ books: this.state.books });
    // 2.遵守不变性原则,新生成一个books
    const books = [...this.state.books];
    books.push(newBook);
    this.setState({ books });
  }
  addCount(index) {
    const books = [...this.state.books];
    books[index].count++;
    this.setState({ books });
  }
  render() {
    const { books } = this.state;
    return (
      <div>
        <button onClick={(e) => this.addNewBook(e)}>添加书籍</button>
        <ul>
          {books.map((item, index) => {
            return (
              <li key={index}>
                {item.name}-{item.price}-{item.count}
                <button onClick={(e) => this.addCount(index)}>+1</button>
              </li>
            );
          })}
        </ul>
      </div>
    );
  }
}

export default App;