Skip to content

React.memo 解决了什么问题?

React.memo 是 React 16.6 版本引入的一个新功能,它是一个高阶组件,旨在优化函数组件的性能。那么,它到底解决了什么问题呢?

React.memo 基础用法

问题:不必要的渲染

在 React 中,组件的重新渲染通常是由于状态或 props 的变化引起的。但有时,即使相关数据没有发生变化,组件也可能会进行不必要的渲染。这种不必要的渲染可能会导致性能下降,尤其是在复杂的应用程序中。

demo 代码
jsx
import { useState } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  console.log("ChildComponent render");
  return <p>{props.text}</p>;
}

function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <ChildComponent text="我是子组件,请在控制台查看打印结果!" />
    </Card>
  );
}

export default ParentComponent;
import { useState } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  console.log("ChildComponent render");
  return <p>{props.text}</p>;
}

function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <ChildComponent text="我是子组件,请在控制台查看打印结果!" />
    </Card>
  );
}

export default ParentComponent;

在这个例子中,ParentComponent有一个状态count,每当我们点击按钮时,这个状态就会增加。由于 React 的工作方式,每次count发生变化时,ParentComponent都会重新渲染。这也意味着ChildComponent也会重新渲染,尽管传递给它的text prop 并没有发生任何变化。

这就是一个不必要的渲染的例子。每次ParentComponent的状态发生变化时,ChildComponent都会进行不必要的渲染,即使它接收的 props 完全相同。

这种情况在大型应用程序中可能会变得更加严重,因为不必要的渲染可能会在多个组件之间产生连锁反应,导致整个应用程序的性能下降。

解决方案:浅比较 props

React.memo 的工作原理是对组件的 props 进行浅比较。如果传递给组件的 props 没有发生变化,React.memo 会复用上一次的渲染结果,从而避免不必要的渲染。

demo 代码
jsx
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  console.log("ChildComponent 渲染");
  return <p>{props.text}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent text="我是 memoized 子组件,请在控制台查看打印结果!" />
    </Card>
  );
}

export default ParentComponent;
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  console.log("ChildComponent 渲染");
  return <p>{props.text}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent text="我是 memoized 子组件,请在控制台查看打印结果!" />
    </Card>
  );
}

export default ParentComponent;

在上述例子中,即使 ParentComponent 重新渲染(例如,由于 count 状态的变化),由于传递给 MemoizedChildComponenttext prop 没有发生变化,ChildComponent 不会重新渲染。如果我们没有使用 React.memo,每次点击按钮时,ChildComponent 都会重新渲染,即使其 props 没有发生变化。

自定义比较函数

React.memo 的第二个参数是一个比较函数,它允许我们自定义比较 props 的逻辑。如果我们传递了一个比较函数,React.memo 将使用它来决定是否重新渲染组件,而不是简单地使用浅比较。

举例说明:

假设我们有一个 User 组件,它接受一个 user 对象和一个 onUpdate 函数作为 props。我们只关心 user 对象中的 id 和 name 字段,而不关心其他字段。此外,我们知道 onUpdate 函数的引用可能会经常改变,但它的实际功能不会改变。

在这种情况下,我们可以使用 React.memo 的第二个参数来提供一个自定义的比较函数:

jsx
const areEqual = (prevProps, nextProps) => {
  // 检查user对象中的id和name字段是否相同
  return (
    prevProps.user.id === nextProps.user.id &&
    prevProps.user.name === nextProps.user.name
  );
};

const User = React.memo(({ user, onUpdate }) => {
  // 组件逻辑
}, areEqual);
const areEqual = (prevProps, nextProps) => {
  // 检查user对象中的id和name字段是否相同
  return (
    prevProps.user.id === nextProps.user.id &&
    prevProps.user.name === nextProps.user.name
  );
};

const User = React.memo(({ user, onUpdate }) => {
  // 组件逻辑
}, areEqual);

在上述例子中,即使 onUpdate 函数的引用或 user 对象中的其他字段发生变化,只要 id 和 name 字段保持不变,User 组件就不会重新渲染。

总之,React.memo 的第二个参数提供了强大的自定义比较逻辑,使我们能够更精确地控制组件的重新渲染行为。

注意事项

传递函数给子组件

当父组件重新渲染时,如果它向子组件传递一个函数,并且这个函数是在父组件的 render 方法或函数组件的主体中定义的,那么每次父组件重新渲染时,这个函数都会被重新创建。这意味着从技术上讲,这个函数在每次渲染时都是创建一个新的函数,即使它的实际功能没有变化。

React.memo 通过浅比较 props 来避免不必要的重新渲染。但是,由于每次都是一个新的函数实例,浅比较会认为函数已经改变,从而导致子组件重新渲染。

demo 代码
jsx
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { getText } = props;
  console.log("ChildComponent render");
  return <p>{getText()}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  const getText = () => {
    return "我是子组件,请在控制台查看打印结果!";
  };

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent getText={getText} />
    </Card>
  );
}

export default ParentComponent;
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { getText } = props;
  console.log("ChildComponent render");
  return <p>{getText()}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  const getText = () => {
    return "我是子组件,请在控制台查看打印结果!";
  };

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent getText={getText} />
    </Card>
  );
}

export default ParentComponent;

在上述案例中,我们观察到以下情况:

当点击 Increment 按钮时,父组件的状态 count 发生变化,导致父组件重新渲染。由于父组件重新渲染,getText 函数也会被重新创建。

由于使用了 memo 对 ChildComponent 进行了包裹,MemoizedChildComponent 会进行浅比较以决定是否重新渲染。由于 getText 函数在每次渲染时都是一个新的实例,浅比较会认为该函数发生了变化,因此会触发子组件的重新渲染。

解决方案

常见的三种解决方案如下:

1、将函数移至组件外部

如果函数不依赖于组件的 props 或 state,您可以将其移至组件外部,这样它就不会在每次渲染时重新创建。

demo 代码
jsx
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { getText } = props;
  console.log("ChildComponent render");
  return <p>{getText()}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

// 在父组件外部定义函数
const getText = () => {
  return "我是子组件,请在控制台查看打印结果!";
};

function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent getText={getText} />
    </Card>
  );
}

export default ParentComponent;
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { getText } = props;
  console.log("ChildComponent render");
  return <p>{getText()}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

// 在父组件外部定义函数
const getText = () => {
  return "我是子组件,请在控制台查看打印结果!";
};

function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent getText={getText} />
    </Card>
  );
}

export default ParentComponent;

2、使用类组件的方法

如果您使用的是类组件,可以将函数作为类的方法,这样它的引用在重新渲染之间就会保持不变。

demo 代码
jsx
import React, { Component } from "react";
import { Card, Button } from "antd";

class ChildComponent extends Component {
  render() {
    const { getText } = this.props;
    console.log("ChildComponent render");
    return <p>{getText()}</p>;
  }
}

const MemoizedChildComponent = React.memo(ChildComponent);

class ParentComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  getText = () => {
    return "我是子组件,请在控制台查看打印结果!";
  };

  render() {
    const { count } = this.state;
    return (
      <Card title="案例 demo">
        <p>Current count: {count}</p>
        <Button
          onClick={() => this.setState({ count: this.state.count + 1 })}
          type="primary"
        >
          Increment
        </Button>
        <MemoizedChildComponent getText={this.getText} />
      </Card>
    );
  }
}

export default ParentComponent;
import React, { Component } from "react";
import { Card, Button } from "antd";

class ChildComponent extends Component {
  render() {
    const { getText } = this.props;
    console.log("ChildComponent render");
    return <p>{getText()}</p>;
  }
}

const MemoizedChildComponent = React.memo(ChildComponent);

class ParentComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  getText = () => {
    return "我是子组件,请在控制台查看打印结果!";
  };

  render() {
    const { count } = this.state;
    return (
      <Card title="案例 demo">
        <p>Current count: {count}</p>
        <Button
          onClick={() => this.setState({ count: this.state.count + 1 })}
          type="primary"
        >
          Increment
        </Button>
        <MemoizedChildComponent getText={this.getText} />
      </Card>
    );
  }
}

export default ParentComponent;

3、使用 useCallback

如果您使用的是函数组件,可以使用 useCallback Hook 来确保函数的引用在重新渲染之间保持不变。

demo 代码
jsx
import { useState, memo, useCallback } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { getText } = props;
  console.log("ChildComponent render");
  return <p>{getText()}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  const getText = useCallback(() => {
    return "我是子组件,请在控制台查看打印结果!";
  }, [/* 依赖列表 */]);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent getText={getText} />
    </Card>
  );
}

export default ParentComponent;
import { useState, memo, useCallback } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { getText } = props;
  console.log("ChildComponent render");
  return <p>{getText()}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  const getText = useCallback(() => {
    return "我是子组件,请在控制台查看打印结果!";
  }, [/* 依赖列表 */]);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent getText={getText} />
    </Card>
  );
}

export default ParentComponent;

在上面的案例中,只有当依赖列表中的值发生变化时,getText 函数才会被重新创建。

传递对象/数组给子组件

当父组件重新渲染时,如果它向子组件传递一个对象,并且这个对象是在父组件的 render 方法或函数组件的主体中定义的,那么每次父组件重新渲染时,这个对象都会被重新创建。这意味着从技术上讲,这个对象在每次渲染时都是创建一个新的对象,即使它的实际功能没有变化。

React.memo 通过浅比较 props 来避免不必要的重新渲染。但是,由于每次都是一个新的对象,浅比较会认为对象已经改变,从而导致子组件重新渲染。

demo 代码
jsx
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { userInfo } = props;
  console.log("ChildComponent render");
  return <p>{userInfo.motto}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  const userInfo = {
    name: "张三",
    age: 18,
    motto: "我是子组件,请在控制台查看打印结果!",
  };

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent userInfo={userInfo} />
    </Card>
  );
}

export default ParentComponent;
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { userInfo } = props;
  console.log("ChildComponent render");
  return <p>{userInfo.motto}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  const userInfo = {
    name: "张三",
    age: 18,
    motto: "我是子组件,请在控制台查看打印结果!",
  };

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent userInfo={userInfo} />
    </Card>
  );
}

export default ParentComponent;

解决方案

常见的三种解决方案如下:

1、将对象移至组件外部

如果对象不依赖于组件的 props 或 state,您可以将其移至组件外部,这样它就不会在每次渲染时重新创建。

demo 代码
jsx
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { userInfo } = props;
  console.log("ChildComponent render");
  return <p>{userInfo.motto}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

const userInfo = {
  name: "张三",
  age: 18,
  motto: "我是子组件,请在控制台查看打印结果!",
};

function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent userInfo={userInfo} />
    </Card>
  );
}

export default ParentComponent;
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { userInfo } = props;
  console.log("ChildComponent render");
  return <p>{userInfo.motto}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

const userInfo = {
  name: "张三",
  age: 18,
  motto: "我是子组件,请在控制台查看打印结果!",
};

function ParentComponent() {
  const [count, setCount] = useState(0);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent userInfo={userInfo} />
    </Card>
  );
}

export default ParentComponent;

2、使用 useState 或 useRef 来存储对象

如果您的对象不经常变化,可以使用 useState 或 useRef 来存储它,这样它的引用在重新渲染之间就会保持不变。

demo 代码
jsx
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { userInfo } = props;
  console.log("ChildComponent render");
  return <p>{userInfo.motto}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [userInfo, setUserInfo] = useState({
    name: "张三",
    age: 18,
    motto: "我是子组件,请在控制台查看打印结果!",
  });

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent userInfo={userInfo} />
    </Card>
  );
}

export default ParentComponent;
import { useState, memo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { userInfo } = props;
  console.log("ChildComponent render");
  return <p>{userInfo.motto}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [userInfo, setUserInfo] = useState({
    name: "张三",
    age: 18,
    motto: "我是子组件,请在控制台查看打印结果!",
  });

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent userInfo={userInfo} />
    </Card>
  );
}

export default ParentComponent;

3、使用 useMemo

在某些情况下,我们希望组件的某些属性发生变化时,组件不会重新渲染。这时,我们可以使用useMemo来返回组件的 memoized 值,从而避免组件的重新渲染。

demo 代码
jsx
import { useState, memo, useMemo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { userInfo } = props;
  console.log("ChildComponent render");
  return <p>{userInfo.motto}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  const userInfo = useMemo(() => {
    return {
      name: "张三",
      age: 18,
      motto: "我是子组件,请在控制台查看打印结果!",
    };
  }, [/* 依赖列表 */]);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent userInfo={userInfo} />
    </Card>
  );
}

export default ParentComponent;
import { useState, memo, useMemo } from "react";
import { Card, Button } from "antd";

function ChildComponent(props) {
  const { userInfo } = props;
  console.log("ChildComponent render");
  return <p>{userInfo.motto}</p>;
}

const MemoizedChildComponent = memo(ChildComponent);

function ParentComponent() {
  const [count, setCount] = useState(0);

  const userInfo = useMemo(() => {
    return {
      name: "张三",
      age: 18,
      motto: "我是子组件,请在控制台查看打印结果!",
    };
  }, [/* 依赖列表 */]);

  return (
    <Card title="案例 demo">
      <p>Current count: {count}</p>
      <Button onClick={() => setCount(count + 1)} type="primary">
        Increment
      </Button>
      <MemoizedChildComponent userInfo={userInfo} />
    </Card>
  );
}

export default ParentComponent;

在上面的代码中,userInfo 只会在组件首次渲染时创建,除非依赖列表中的值发生变化。

调用 React.memo 后大致执行情况

null