© Bohua Xu

Memoization

Aug 25, 2022 · 9min

在 React 应用的上下文中,Memoization 是一种手段,每当父组件重新渲染时,子组件仅在它所依赖的 props 发生变化时才会重新渲染。如果子组件所依赖的 props 中没有更改,则它不会执行 render 方法,并将返回缓存的结果。由于渲染方法未执行,因此不会有虚拟 DOM 创建和差异检查,从而实现性能的提升。

现在,让我们看看如何在类和函数组件中实现 Memoization,以避免这种不必要的重新渲染。

函数组件实现 Memoization

为了在函数组件中实现 Memoization,我们将使用 React.memo()React.memo() 是一个高阶组件(HOC),它执行与 PureComponent 类似的工作,来避免不必要的重新渲染。

以下是函数组件的代码:

//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>{props.name}</h2>
    </div>
  );
}

export default React.memo(Child); // 这里我们给子组件添加 HOC 实现 Memoization
//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>{props.name}</h2>
    </div>
  );
}

export default React.memo(Child); // 这里我们给子组件添加 HOC 实现 Memoization

类组件实现 Memoization

为了在类组件中实现 Memoization,我们将使用 React.PureComponentReact.PureComponent 实现了 shouldComponentUpdate(),它对 stateprops 进行了浅比较,并且仅在 props 或 state 发生更改时才重新渲染 React 组件。

将子组件更改为如下所示的代码:

//Child.js
class Child extends React.PureComponent { // 这里我们把 React.Component 改成了 React.PureComponent
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>{this.props.name}</h2>
      </div>
    );
  }
}

export default Child;
复制代码
//Child.js
class Child extends React.PureComponent { // 这里我们把 React.Component 改成了 React.PureComponent
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>{this.props.name}</h2>
      </div>
    );
  }
}

export default Child;
复制代码

此示例的完整代码显示在这个 sandbox 中。

父组件保持不变。现在,当我们在父组件中增加 count 时,控制台中的输出如下所示:

Parent render
Child render
Parent render
Parent render
复制代码
Parent render
Child render
Parent render
Parent render
复制代码

对于首次渲染,它同时调用父组件和子组件的 render 方法。

对于每次增加 count 后的重新渲染,仅调用父组件的 render 函数。子组件不会重新渲染。

React.memo() 存在的问题(使用useMemo也可以处理)

useCallback是uesMemo的一个语法糖(更好的处理函数_直接写)

在上面的示例中,我们看到,当我们对子组件使用 React.memo() HOC 时,子组件没有重新渲染,即使父组件重新渲染了。

但是,需要注意的一个小问题是,如果我们将函数作为参数传递给子组件,即使在使用 React.memo() 之后,子组件也会重新渲染。让我们看一个这样的例子。

我们将更改父组件,如下所示。在这里,我们添加了一个处理函数,并作为参数传递给子组件:

//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };

  const handler = () => {
    console.log("handler");    // 这里的 handler 函数将会被传递给子组件
  };

  console.log("Parent render");
  return (
    <div className="App">
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} childFunc={handler} />
    </div>
  );
}
复制代码
//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };

  const handler = () => {
    console.log("handler");    // 这里的 handler 函数将会被传递给子组件
  };

  console.log("Parent render");
  return (
    <div className="App">
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} childFunc={handler} />
    </div>
  );
}
复制代码

子组件代码将保持原样。我们不会在子组件中使用父组件传递来的函数:

//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>{props.name}</h2>
    </div>
  );
}

export default React.memo(Child);
复制代码
//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>{props.name}</h2>
    </div>
  );
}

export default React.memo(Child);
复制代码

现在,当我们递增父组件中的 count 时,它会重新渲染并同时重新渲染子组件,即使传递的参数中没有更改。

那么,是什么原因导致子组件重新渲染的呢?答案是,每次父组件重新渲染时,都会创建一个新的 handler 函数并将其传递给子组件。现在,由于每次重新渲染时都会重新创建 handle 函数,因此子组件在对 props 进行浅比较时会发现 handler 引用已更改,并重新渲染子组件。

通过 useCallback() 来避免更多的重复渲染

导致子组件重新渲染的主要问题是重新创建了 handler 函数,这更改了传递给子组件的引用。因此,我们需要有一种方法来避免这种重复创建。如果未重新创建 handler 函数,则对 handler 函数的引用不会更改,因此子组件不会重新渲染。

为了避免每次渲染父组件时都重新创建函数,我们将使用一个名为 useCallback() 的 React Hook。Hooks 是在 React 16 中引入的。要了解有关 Hooks 的更多信息,你可以查看 React 的官方 hooks 文档,或者查看 `React Hooks: How to Get Started & Build Your Own"。

useCallback() 钩子传入两个参数:回调函数和依赖项列表。

以下是 useCallback() 示例:

const handleClick = useCallback(() => {
}, [x,y]);
复制代码
const handleClick = useCallback(() => {
}, [x,y]);
复制代码

在这里,useCallback() 被添加到 handleClick() 函数中。第二个参数 [x, y] 可以是空数组、单个依赖项或依赖项列表。每当第二个参数中提到的任何依赖项发生更改时,才会重新创建 handleClick() 函数。

如果 useCallback() 中提到的依赖项没有更改,则返回作为第一个参数提及的回调函数的 Memoization 版本。我们将更改父组件,以便对传递给子组件的处理程序使用 useCallback() 钩子:

//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };

  const handler = useCallback(() => { // 给 handler 函数使用 useCallback()
    console.log("handler");
  }, []);

  console.log("Parent render");
  return (
    <div className="App">
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} childFunc={handler} />
    </div>
  );
}
复制代码
//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };

  const handler = useCallback(() => { // 给 handler 函数使用 useCallback()
    console.log("handler");
  }, []);

  console.log("Parent render");
  return (
    <div className="App">
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} childFunc={handler} />
    </div>
  );
}
复制代码

子组件代码将保持原样。

此示例的完整代码这个 sandbox 中。

当我们在上述代码的父组件中增加 count 时,我们可以看到以下输出:

Parent render
Child render
Parent render
Parent render
Parent render
复制代码
Parent render
Child render
Parent render
Parent render
Parent render
复制代码

由于我们对父组件中的 handler 使用了 useCallback() 钩子,因此每次父组件重新渲染时,都不会重新创建 handler 函数,并且会将 handler 的 Memoization 版本传递到子组件。子组件将进行浅比较,并注意到 handler 函数的引用没有更改,因此它不会调用 render 方法。

值得注意的事

Memoization 是一种很好的手段,可以避免在组件的 state 或 props 没有改变时对组件进行不必要的重新渲染,从而提高 React 应用的性能。你可能会考虑为所有组件添加 Memoization,但这并不一定是构建高性能 React 组件的方法。只有在组件出现以下情况时,才应使用 Memoization:

  • 固定的输入有固定的输出时
  • 具有较多 UI 元素,虚拟 DOM 检查将影响性能
  • 多次传递相同的参数

总结

在本教程中,我们理解了:

  • React 是如何渲染 UI 的
  • 为什么需要 Memoization
  • 如何在 React 中通过函数组件的 React.memo() 和类组件的 React.PureComponent 实现 Memoization
  • 通过一个例子展示,即使在使用 React.memo() 之后,子组件也会重新渲染
  • 如何使用 useCallback() 钩子来避免在函数作为 props 传递给子组件时产生重新渲染的问题

斐波那契数列 缓存

CC BY-NC-SA 4.0 2021-PRESENT © Bohua Xu