原文链接: Improving Performance in React Functional Component using React.memo

译者: 进击的大葱

React 核心开发团队一直都努力地让 React 变得更快。在 React 中可以用来优化组件性能的方法大概有以下几种:

  • 组件懒加载 ( React.lazy(...)<Suspense /> )
  • Pure Component
  • shouldComponentUpdate(...){...} 生命周期函数

本文还会介绍 React16.6 加入的另外一个专门用来优化 函数组件 (Functional Component) 性能的方法: React.memo

无用的渲染

组件是构成 React 视图的一个基本单元。有些组件会有自己本地的状态 (state), 当它们的值由于用户的操作而发生改变时,组件就会重新渲染。在一个 React 应用中,一个组件可能会被频繁地进行渲染。这些渲染虽然有一小部分是必须的,不过大多数都是无用的,它们的存在会大大降低我们应用的性能。

看下面这个例子:

  1. import React from "react";
  2. class TestC extends React.Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = {
  6. count: 0
  7. };
  8. }
  9. componentWillUpdate(nextProps, nextState) {
  10. console.log("componentWillUpdate");
  11. }
  12. componentDidUpdate(prevProps, prevState) {
  13. console.log("componentDidUpdate");
  14. }
  15. render() {
  16. return (
  17. <div>
  18. {this.state.count}
  19. <button onClick={() => this.setState({ count: 1 })}>Click Me</button>
  20. </div>
  21. );
  22. }
  23. }
  24. export default TestC;

TestC 组件有一个本地状态 count,它的初始值是 0(state = {count: 0})。当我们点击 Click Me 按钮时,count 的值被设置为 1。这时候屏幕的数字将会由 0 变成 1。当我们再次点击该按钮时,count 的值还是 1, 这时候 TestC 组件不应该被重新渲染,可是现实是这样的吗?

为了测试 count 重复设置相同的值组件会不会被重新渲染, 我为 TestC 组件添加了两个生命周期函数: componentWillUpdate 和 componentDidUpdate。componentWillUpdate 方法在组件将要被重新渲染时被调用,而 componentDidUpdate 方法会在组件成功重渲染后被调用。

在浏览器中运行我们的代码,然后多次点击 Click Me 按钮,你可以看到以下输出:

[译] 使用 React.memo() 来优化函数组件的性能 - 图1

我们可以看到’componentWillUpdate’和’componentWillUpdate’在每次我们点击完按钮后,都会在控制台输出来。所以即使 count 被设置相同的值,TestC 组件还是会被重新渲染,这些就是所谓的无用渲染。

Pure Component/shouldComponentUpdate

为了避免 React 组件的无用渲染,我们可以实现自己的 shouldComponentUpdate 生命周期函数。

当 React 想要渲染一个组件的时候,它将会调用这个组件的 shouldComponentUpdate 函数, 这个函数会告诉它是不是真的要渲染这个组件。

如果我们的 shouldComponentUpdate 函数这样写:

  1. shouldComponentUpdate(nextProps, nextState) {
  2. return true
  3. }

其中各个参数的含义是:

  • nextProps: 组件将会接收的下一个参数 props
  • nextProps: 组件的下一个状态 state

因为我们的 shouldComponentUpdate 函数一直返回 true,这就告诉 React,无论何种情况都要重新渲染该组件。

可是如果我们这么写:

  1. shouldComponentUpdate(nextProps, nextState) {
  2. return false
  3. }

因为这个方法的返回值是 false,所以 React 永远都不会重新渲染我们的组件。

因此当你想要 React 重新渲染你的组件的时候,就在这个方法中返回 true,否则返回 false。现在让我们用 shouldComponentUpdate 重写之前的 TestC 组件:

  1. import React from "react";
  2. class TestC extends React.Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = {
  6. count: 0
  7. };
  8. }
  9. componentWillUpdate(nextProps, nextState) {
  10. console.log("componentWillUpdate");
  11. }
  12. componentDidUpdate(prevProps, prevState) {
  13. console.log("componentDidUpdate");
  14. }
  15. shouldComponentUpdate(nextProps, nextState) {
  16. if (this.state.count === nextState.count) {
  17. return false;
  18. }
  19. return true;
  20. }
  21. render() {
  22. return (
  23. <div>
  24. {this.state.count}
  25. <button onClick={() => this.setState({ count: 1 })}> Click Me </button>
  26. </div>
  27. );
  28. }
  29. }
  30. export default TestC;

我们在 TestC 组件里添加了 shouldComponentUpdate 方法,判断如果现在状态的 count 和下一个状态的 count 一样时,我们返回 false,这样 React 将不会进行组件的重新渲染,反之,如果它们两个的值不一样,就返回 true,这样组件将会重新进行渲染。

再次在浏览器中测试我们的组件,刚开始的界面是这样的:

[译] 使用 React.memo() 来优化函数组件的性能 - 图2

这时候,就算我们多次点击 Click Me 按钮,也只能看到两行输出:

  1. componentWillUpdate
  2. componentDidUpdate

因为第二次点击 Click Me 按钮后 count 值一直是 1,这样 shouldComponentUpdate 一直返回 false,所以组件就不再被重新渲染了。

那么如何验证后面 state 的值发生改变,组件还是会被重新渲染呢?我们可以在浏览器的 React DevTools 插件中直接对 TestC 组件的状态进行更改。具体做法是, 在 Chrome 调试工具中点击 React 标签,在界面左边选中 TestC 组件,在界面的右边就可以看到其状态 state 中只有一个键 count,且其值是 1:

[译] 使用 React.memo() 来优化函数组件的性能 - 图3

然后让我们点击 count 的值 1,将其修改为 2,然后按回车键:

[译] 使用 React.memo() 来优化函数组件的性能 - 图4

你将会看到控制台有以下输出:

  1. componentWillUpdate;
  2. componentDidUpdate;
  3. componentWillUpdate;
  4. componentDidUpdate;

state 的 count 被改变了,组件也被重新渲染了。

现在让我们使用另外一种方法 PureComponent 来对组件进行优化。

React 在 v15.5 的时候引入了 Pure Component 组件。React 在进行组件更新时,如果发现这个组件是一个 PureComponent,它会将组件现在的 state 和 props 和其下一个 state 和 props 进行 浅比较 ,如果它们的值没有变化,就不会进行更新。要想让你的组件成为 Pure Component,只需要 extends React.PureComponent 即可。

让我们用 PureComponent 去改写一下我们的代码吧:

  1. import React from 'react';
  2. class TestC extends React.PureComponent {
  3. constructor(props) {
  4. super(props);
  5. this.state = {
  6. count: 0
  7. }
  8. }
  9. componentWillUpdate(nextProps, nextState) {
  10. console.log('componentWillUpdate')
  11. }
  12. componentDidUpdate(prevProps, prevState) {
  13. console.log('componentDidUpdate')
  14. }
  15. /*shouldComponentUpdate(nextProps, nextState) {
  16. if (this.state.count === nextState.count) {
  17. return false
  18. }
  19. return true
  20. }*/
  21. render() {
  22. return (
  23. <div>
  24. {this.state.count}
  25. <button onClick = {
  26. () => this.setState({ count: 1 })
  27. }> Click Me </button>
  28. </div>
  29. );
  30. }
  31. }
  32. export default TestC;

在上面的代码中,我将 shouldComponentUpdate 的代码注释掉了,因为 React.PureComponent 本身就帮我们实现了一样的功能。

改完代码后,我们刷新一下浏览器,然后多次点击 Click Me 按钮看组件被渲染了多少遍:

[译] 使用 React.memo() 来优化函数组件的性能 - 图5

由上面的输出可知,我们的 component 只在 state 由 0 变为 1 时被重新渲染了,后面都没有进行渲染。

函数组件

上面我们探讨了如何使用 PureComponentshouldComponentUpdate 的方法优化类组件的性能。虽然类组件是 React 应用的主要组成部分,不过函数组件 (Functional Component) 同样可以被作为 React 组件使用。

  1. function TestC(props) {
  2. return <div>I am a functional component</div>;
  3. }

对于函数组件,它们没有诸如 state 的东西去保存它们本地的状态 (虽然在 React Hooks 中函数组件可以使用 useState 去使用状态), 所以我们不能像在类组件中使用 shouldComponentUpdate 等生命函数去控制函数组件的重渲染。当然,我们也不能使用 extends React.PureComponent 了,因为它压根就不是一个类。

要探讨解决方案,让我们先验证一下函数组件是不是也有和类组件一样的无用渲染的问题。

首先我们先将 ES6 的 TestC 类转换为一个函数组件:

  1. import React from 'react';
  2. const TestC = (props) => {
  3. console.log(`Rendering TestC :` props)
  4. return (
  5. <div>
  6. {props.count}
  7. </div>
  8. )
  9. }
  10. export default TestC;
  11. // App.js
  12. <TestC count={5} />

当上面的代码初次加载时,控制台的输出是:

[译] 使用 React.memo() 来优化函数组件的性能 - 图6

同样,我们可以打开 Chrome 的调试工具,点击 React 标签然后选中 TestC 组件:

[译] 使用 React.memo() 来优化函数组件的性能 - 图7

我们可以看到这个组件的参数值是 5,让我们将这个值改为 45, 这时候浏览器输出:

[译] 使用 React.memo() 来优化函数组件的性能 - 图8

由于 count 的值改变了,所以该组件也被重新渲染了,控制台输出 Object{count: 45},让我们重复设置 count 的值为 45, 然后再看一下控制台的输出结果:

[译] 使用 React.memo() 来优化函数组件的性能 - 图9

由输出结果可以看出,即使 count 的值保持不变,还是 45, 该组件还是被重渲染了。

既然函数组件也有无用渲染的问题,我们如何对其进行优化呢?

解决方案: 使用 React.memo()

React.memo(...) 是 React v16.6 引进来的新属性。它的作用和 React.PureComponent 类似,是用来控制函数组件的重新渲染的。React.memo(...) 其实就是函数组件的 React.PureComponent

如何使用 React.memo(…)?

React.memo 使用起来非常简单,假设你有以下的函数组件:

  1. const Funcomponent = () => {
  2. return <div>Hiya!! I am a Funtional component</div>;
  3. };

我们只需将上面的 Funcomponent 作为参数传入 React.memo 中:

  1. const Funcomponent = () => {
  2. return <div>Hiya!! I am a Funtional component</div>;
  3. };
  4. const MemodFuncComponent = React.memo(FunComponent);

React.memo 会返回一个 纯化 (purified) 的组件 MemoFuncComponent,这个组件将会在 JSX 标记中渲染出来。当组件的参数 props 和状态 state 发生改变时,React 将会检查前一个状态和参数是否和下一个状态和参数是否相同,如果相同,组件将不会被渲染,如果不同,组件将会被重新渲染。

现在让我们在 TestC 组件上使用 React.memo 进行优化:

  1. let TestC = props => {
  2. console.log("Rendering TestC :", props);
  3. return <div>{props.count}</>;
  4. };
  5. TestC = React.memo(TestC);

打开浏览器重新加载我们的应用。然后打开 Chrome 调试工具,点击 React 标签,然后选中 <Memo(TestC)> 组件。

接着编辑一下 props 的值,将 count 改为 89,我们将会看到我们的应用被重新渲染了:

[译] 使用 React.memo() 来优化函数组件的性能 - 图10

然后重复设置 count 的值为 89:

[译] 使用 React.memo() 来优化函数组件的性能 - 图11

这里没有重新渲染!

这就是 React.memo(...) 这个函数牛 X 的地方!

在我们之前那个没用到 React.memo(...) 的例子中,count 的重复设置会使组件进行重新渲染。可是我们用了 React.memo 后,该组件在传入的值不变的前提下是不会被重新渲染的。

结论

以下是几点总结:

  • React.PureComponent 是银
  • React.memo(...) 是金
  • React.PureComponent 是给 ES6 的类组件使用的
  • React.memo(...) 是给函数组件使用的
  • React.PureComponent 减少 ES6 的类组件的无用渲染
  • React.memo(...) 减少函数组件的无用渲染
  • 为函数组件提供优化是一个巨大的进步