memo()、useCallback()、useMemo()使用场景

9,585 阅读4分钟

前置知识

了解基本数据类型与引用数据类型

'a' === 'a'         // true

{} === {}                 // false

API

  • React.memo()
  • React.useCallback()
  • React.useMemo()

React.memo()

先看问题

React 中当组件的 props 或 state 变化时,会重新渲染,实际开发会遇到不必要的渲染场景。比如:
父组件:

import { useState } from "react";
import { Child } from "./child";

export const Parent = () => {
  const [count, setCount] = useState(0);
  const increment = () => setCount(count + 1);

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <Child />
    </div>
  );
};


子组件:

export const Child = ({}) => {
  console.log("渲染了");
  return <div>子组件</div>;
};

点击父组件中按钮,会修改 count 变量的值,进而导致父组件重新渲染,此时子组件没有任何变化(props、state),但在控制台中仍然看到子组件被渲染的打印信息。

image.png 子组件的 props 和 state 没有变化,我们并不希望它重现渲染。

如何解决?(React.memo())

React.memo()是React v16.6引进来的新属性,用来控制函数组件的重新渲染。
React.memo()将组件作为函数(memo)的参数,函数的返回值(Child)是一个新的组件。
子组件:

import { memo } from "react";

export const Child = memo(() => {
  console.log("渲染了");
  return <div>子组件</div>;
});

在看下效果:

image.png 显而易见,并没有重新渲染子组件。

React.useCallback()

先看问题

上面的例子中,父组件只是简单调用子组件,并未给子组件传递任何属性。我们传值看看:
父组件:

import { useState } from "react";
import { Child } from "./child";

export const Parent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("小明");
  const increment = () => setCount(count + 1);

  const onClick = (name: string) => {
    setName(name);
  };

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <Child name={name} onClick={onClick} />
    </div>
  );
};


子组件:

import { memo } from "react";

export const Child = memo(
  (props: { name: string; onClick: (value: any) => void }) => {
    const { name, onClick } = props;
    console.log("渲染了", name, onClick);
    return (
      <>
        <div>子组件</div>
        <button onClick={() => onClick("小红")}>改变 name 值</button>
      </>
    );
  }
);

点击父组件count,看到子组件每次都重新渲染了。

image.png 分析下原因:

  • 点击父组件按钮,改变了父组件中 count 变量,进而导致父组件重新渲染;
  • 父组件重新渲染时,会重新创建 onClick 函数,即传给子组件的 onClick 属性发生了变化,导致子组件渲染;
  • 如果传给子组件的props只有基本数据类型将不会重新渲染。

注意: 如果直接使用useState解构的setName传给子组件, 子组件将不会重复渲染,因为解构出来的是一个memoized 函数。

import { useState } from "react";
import { Child } from "./child";

export const Parent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("小明");
  const increment = () => setCount(count + 1);

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <Child name={name} setName={setName} />
    </div>
  );
};

如何解决?(React.useCallback())

把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的memoized回调函数,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate)的子组件时,它将非常有用。\

  • memoized回调函数: 使用一组参数初次调用函数时,缓存参数和计算结果,当再次使用相同的参数调用该函数时,直接返回相应的缓存结果。(返回对应饮用,所以恒等于 ===)

注意: 依赖项数组不会作为参数传给回调函数。虽然从概念上来说它表现为:所有回调函数中引用的值都应该出现在依赖项数组中。

修改父组件的 onClick 方法,用 useCallback 钩子函数包裹一层。

import { useCallback, useState } from "react";
import { Child } from "./child";

export const Parent = () => {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("小明");
  const increment = () => setCount(count + 1);

  const onClick = useCallback((name: string) => {
    setName(name);
  }, []);

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <Child name={name} onClick={onClick} />
    </div>
  );
};

点击父组件count,子组件将不会重新渲染。

React.useMemo()

先看问题

上面的例子中,name 属性是个字符串,如果换成传递对象会怎样?
父组件:

import { useCallback, useState } from "react";
import { Child } from "./child";

export const Parent = () => {
  const [count, setCount] = useState(0);
  // const [userInfo, setUserInfo] = useState({ name: "小明", age: 18 });
  const increment = () => setCount(count + 1);
  const userInfo = { name: "小明", age: 18 };

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <Child userInfo={userInfo} />
    </div>
  );
};


子组件:

import { memo } from "react";

export const Child = memo(
  (props: { userInfo: { name: string; age: number } }) => {
    const { userInfo } = props;
    console.log("渲染了", userInfo);
    return (
      <>
        <div>名字: {userInfo.name}</div>
        <div>年龄:{userInfo.age}</div>
      </>
    );
  }
);

点击父组件count,看到子组件每次都重新渲染了。 分析原因跟调用函数是一样的:

  • 点击父组件按钮,触发父组件重新渲染;
  • 父组件渲染,const userInfo = { name: "小明", age: 18 }; 一行会重新生成一个新对象,导致传递给子组件的 userInfo 属性值变化,进而导致子组件重新渲染。
  • 注意: 如果使用useState解构的userInfo, 子组件将不会重复渲染,因为解构出来的是一个memoized 值。

如何解决?(React.useMemo())

使用 useMemo 将对象属性包一层。

import { useMemo, useState } from "react";
import { Child } from "./child";

export const Parent = () => {
  const [count, setCount] = useState(0);
  // const [userInfo, setUserInfo] = useState({ name: "小明", age: 18 });
  const increment = () => setCount(count + 1);
  const userInfo = useMemo(() => ({ name: "小明", age: 18 }), []);

  return (
    <div>
      <button onClick={increment}>点击次数:{count}</button>
      <Child userInfo={userInfo} />
    </div>
  );
};

useMemo()返回一个 memoized 值。

把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。

记住,传入 useMemo 的函数会在渲染期间执行。请不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect 的适用范畴,而不是 useMemo

如果没有提供依赖项数组,useMemo 在每次渲染时都会计算新的值。