React.memo 使用的 4 种误区

react 中组件缓存的两种方式:

  1. 使用 memo 缓存子组件

    js 复制代码
    const App = () => {
      const [count, setCount] = useState(0);
      const onClick = () => setCount(count + 1);
      return (
        <>
          <div>
            {count}
            <button onClick={onClick}>按钮</button>
          </div>
          <Child />
        </>
      );
    };
    const Child = memo(() => {
      console.log("child render");
      return <div>Child</div>;
    });
  2. 使用 useMemo 缓存组件

    js 复制代码
    const App = () => {
      const [count, setCount] = useState(0);
      const onClick = () => setCount(count + 1);
      const ChildMemo = useMemo(() => <Child />, []);
      return (
        <>
          <div>
            {count}
            <button onClick={onClick}>按钮</button>
          </div>
          {ChildMemo}
        </>
      );
    };
    const Child = () => {
      console.log("child render");
      return <div>Child</div>;
    };

这两种方式的区别是:

  • memo 是通过比较 prevPropsnextProps 是否相同来决定是否更新
  • useMemo 是通过依赖是否变化来决定是否更新

什么时候使用 useMemo 缓存组件,什么时候使用 memo 缓存组件呢?

大部分情况都是使用 memo 取缓存组件的,因为 memo 的使用更简单,而且 memo 也是 react 官方推荐的

使用 useMemo 缓存的组件的话,可以用于切换组件的封装,比如下面的代码:

js 复制代码
{
  count % 2 === 0 ? <Child1 /> : <Child2 />;
}
🔽
const element = useMemo(() => (count % 2 === 0 ? <Child1 /> : <Child2 />), [count]);

在使用 memo 缓存组件时有些坑要注意,比如下面这几种

缓存失效1------未使用 useCallback

子组件使用了 memo,但是父组件没有使用 useCallback 包裹 onClick 函数,这样 memo 就失效了

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const onClick = () => console.log("app click");
  const onClickAppButton = () => setCount(count + 1);
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} />
    </div>
  );
}

const Child = memo(({ onClick }) => {
  console.log("child render"); // 父组件更新,子组件也会更新
  return <div>Child</div>;
});

因为在父组件中,每次 setCount,都会创建一个新的 onClick 函数,而 memo 是通过比较 prevPropsnextProps 来决定是否更新的,所以每次父组件更新,子组件也会更新

正确的写法是,在父组件中将 onClick 使用 useCallback 缓存起来

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => setCount(count + 1);
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} />
    </div>
  );
}

const Child = memo(({ onClick }) => {
  console.log("child render"); // 父组件更新,子组件不会更新了
  return <div>Child</div>;
});

useCallback 错误使用

这种方式不会造成缓存失效,而是拿不到最新的状态

子组件中使用了 useCallback 缓存函数,但是依赖是空数组(或者有依赖忘记写了)

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  // count 永远是初始值 0
  const onClick = useCallback(() => console.log("app click", count), [count]);
  const onClickAppButton = () => setCount(count + 1);
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} />
    </div>
  );
}

const Child = memo(({ onClick }) => {
  const [childCount, setChildCount] = useState(0);
  const onClickChildButton = useCallback(() => {
    setChildCount((childCount) => childCount + 1);
    onClick();
  }, []);
  return (
    <div>
      Child---{childCount}
      <button onClick={onClickChildButton}>child 按钮</button>
    </div>
  );
});

子组件中的依赖是空数组,它无法感知父组件 count 的变化

正确的做法是将将 onClick 放入 useCallback 的依赖项里

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  // count 永远是初始值 0
  const onClick = useCallback(() => console.log("app click", count), [count]);
  const onClickAppButton = () => setCount(count + 1);
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} />
    </div>
  );
}

const Child = memo(({ onClick }) => {
  const [childCount, setChildCount] = useState(0);
  const onClickChildButton = useCallback(() => {
    setChildCount((childCount) => childCount + 1);
    onClick();
  }, [onClick]);
  return (
    <div>
      Child---{childCount}
      <button onClick={onClickChildButton}>child 按钮</button>
    </div>
  );
});

缓存失效------props 使用普通对象

将对象直接写在 jsx 的属性中,这种和上面的 useCallback 错误使用 1 是一样的,每次父组件更新,都会创建一个新的 style 对象,导致子组件更新

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => setCount(count + 1);
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} style={{ color: "red" }} />
    </div>
  );
}

const Child = memo(({ onClick, style }) => {
  console.log("child render"); // 父组件更新,子组件也会更新
  return <div style={style}>Child</div>;
});

正确的写法使用使用 useMemostyle 缓存起来

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => setCount(count + 1);
  const style = useMemo(() => ({ color: "red" }), []); // 这里使用了 useMemo
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} style={style} />
    </div>
  );
}

const Child = memo(({ onClick, style }) => {
  console.log("child render"); // 父组件更新,子组件不会更新了
  return <div style={style}>Child</div>;
});

当然也可以使用 useState 定义对象

js 复制代码
function App() {
  const [count, setCount] = useState(0);
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => setCount(count + 1);
  const [style] = useState({ color: "red" }); // 这里使用 useState
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton}>app 按钮</button>
      <Child onClick={onClick} style={style} />
    </div>
  );
}

const Child = memo(({ onClick, style }) => {
  console.log("child render"); // 父组件更新,子组件不会更新了
  return <div style={style}>Child</div>;
});

缓存失效3------第三方状态管理工具

使用第三方状态管理工具,比如 zustand,在使用 memo 缓存组件时也会遇到失效的情况

如下代码:

  1. styleParent 是父组件使用的样式,styleChild 是子组件使用的样式
  2. 父组件中点击按钮,会更新 styleParent,同时子组件也会重新渲染,导致 memo 失效了
js 复制代码
const useStyle = create((set) => ({
  styleChild: { color: "red" },
  styleParent: { color: "blue" },
  setStyle: (color) => set({ styleParent: { color } }),
}));
function App() {
  const [count, setCount] = useState(0);
  const { setStyle, styleParent } = useStyle();
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => {
    setCount(count + 1);
    setStyle(styleParent.color === "blue" ? "green" : "blue");
  };
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton} style={styleParent}>
        app 按钮
      </button>
      <Child onClick={onClick} />
    </div>
  );
}
const Child = memo(() => {
  console.log("child render"); // 父组件更新,子组件也会更新
  const { styleChild } = useStyle();
  return <div style={styleChild}>Child</div>;
});

如果要避免 memo 失效,有两种方式:

  1. 不要使用解构的语法,而是在 useStyle 中将 styleChildstate 中提取出来

    js 复制代码
    const useStyle = create((set) => ({
      styleChild: { color: "red" },
      styleParent: { color: "blue" },
      setStyleParent: (color) => set({ styleParent: { color } }),
    }));
    function App() {
      const [count, setCount] = useState(0);
      const { setStyleParent, styleParent } = useStyle();
      const onClick = useCallback(() => console.log("app click"), []);
      const onClickAppButton = () => {
        setCount(count + 1);
        setStyleParent(styleParent.color === "blue" ? "green" : "blue");
      };
      return (
        <div>
          app---{count}
          <button onClick={onClickAppButton} style={styleParent}>
            app 按钮
          </button>
          <Child onClick={onClick} />
        </div>
      );
    }
    const Child = memo(() => {
      console.log("child render"); // 父组件更新,子组件不会更新
      // 在 useStyle 中将 styleChild 从 state 中提取出来
      const styleChild = useStyle((state) => state.styleChild);
      return <div style={styleChild}>Child</div>;
    });
  2. 子组件中不使用 useStyle,而是将 styleChild 从父组件中传递过来

    js 复制代码
    const useStyle = create((set) => ({
      styleChild: { color: "red" },
      styleParent: { color: "blue" },
      setStyle: (color) => set({ styleParent: { color } }),
    }));
    function App() {
      const [count, setCount] = useState(0);
      const { setStyle, styleParent, styleChild } = useStyle();
      const onClick = useCallback(() => console.log("app click"), []);
      const onClickAppButton = () => {
        setCount(count + 1);
        setStyle(styleParent.color === "blue" ? "green" : "blue");
      };
      return (
        <div>
          app---{count}
          <button onClick={onClickAppButton} style={styleParent}>
            app 按钮
          </button>
          // 通过 props 传递 styleChild
          <Child onClick={onClick} style={styleChild} />
        </div>
      );
    }
    const Child = memo(({ style }) => {
      console.log("child render"); // 父组件更新,子组件不会更新了
      return <div style={style}>Child</div>;
    });

推荐使用第一种方式,毕竟使用第二种方式的话 zustand 就有点浪费了

缓存失效4------Context

使用 Context 后也会存在缓存失效的情况

context 提供 styleChildstyleParent 两个样式,styleChild 是子组件使用的样式,styleParent 是父组件使用的样式

子组件中如果直接使用 useContext 的话,就会导致 memo 失效

js 复制代码
const StyleContext = createContext(null);
const StyleProvider = ({ children }) => {
  const [styleChild, setStyleChild] = useState({ color: "red" });
  const [styleParent, setStyleParent] = useState({ color: "blue" });
  const style = useMemo(() => ({ styleChild, styleParent, setStyleParent }), [styleChild, styleParent, setStyleParent]);
  return <StyleContext.Provider value={style}>{children}</StyleContext.Provider>;
};
const useStyle = () => useContext(StyleContext);
const App = () => (
  <StyleProvider>
    <Parent />
  </StyleProvider>
);
const Parent = memo(() => {
  const [count, setCount] = useState(0);
  const { styleParent, setStyleParent, styleChild } = useStyle();
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => {
    setCount(count + 1);
    setStyleParent((style) => (style.color === "green" ? { color: "blue" } : { color: "green" }));
  };
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton} style={styleParent}>
        app 按钮
      </button>
      <Child onClick={onClick} style={styleChild} />
    </div>
  );
});
const Child = memo(({ style }) => {
  console.log("child render"); // 父组件更新,子组件也会更新
  return <div style={style}>Child</div>;
});

解决 memo 失效的方式还是要通过 props 传值

js 复制代码
const StyleContext = createContext(null);
const StyleProvider = ({ children }) => {
  const [styleChild, setStyleChild] = useState({ color: "red" });
  const [styleParent, setStyleParent] = useState({ color: "blue" });
  const style = useMemo(() => ({ styleChild, styleParent, setStyleParent }), [styleChild, styleParent, setStyleParent]);
  return <StyleContext.Provider value={style}>{children}</StyleContext.Provider>;
};
const useStyle = () => useContext(StyleContext);
const App = () => (
  <StyleProvider>
    <Parent />
  </StyleProvider>
);
function Parent() {
  const [count, setCount] = useState(0);
  const { styleParent, setStyleParent, styleChild } = useStyle();
  const onClick = useCallback(() => console.log("app click"), []);
  const onClickAppButton = () => {
    setCount(count + 1);
    setStyleParent((style) => (style.color === "green" ? { color: "blue" } : { color: "green" }));
  };
  return (
    <div>
      app---{count}
      <button onClick={onClickAppButton} style={styleParent}>
        app 按钮
      </button>
      <Child onClick={onClick} style={styleChild} />
    </div>
  );
}
const Child = memo(({ style }) => {
  console.log("child render"); // 父组件更新,子组件不会更新
  return <div style={style}>Child</div>;
});

总结

在开发过程中如果考虑性能优化的话,心智负担会太重,所以一般情况下不用考虑,只有在遇到性能问题时,再考虑优化

毕竟 react 自身已经做了很多优化了,比如 fiber 架构下的 setState 是异步的,react 会将多次 setState 合并成一次

所以在开发过程中,不需要考虑要不要使用 useCallbackuseMemomemo,遇到问题时在考虑也不迟

相关推荐
多多米10051 小时前
初学Vue(2)
前端·javascript·vue.js
柏箱1 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑1 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
hmz8561 小时前
最新网课搜题答案查询小程序源码/题库多接口微信小程序源码+自带流量主
前端·微信小程序·小程序
看到请催我学习1 小时前
内存缓存和硬盘缓存
开发语言·前端·javascript·vue.js·缓存·ecmascript
blaizeer2 小时前
深入理解 CSS 浮动(Float):详尽指南
前端·css
编程老船长2 小时前
网页设计基础 第一讲:软件分类介绍、工具选择与课程概览
前端
编程老船长2 小时前
网页设计基础 第二讲:安装与配置 VSCode 开发工具,创建第一个 HTML 页面
前端
速盾cdn2 小时前
速盾:网页游戏部署高防服务器有什么优势?
服务器·前端·web安全
小白求学12 小时前
CSS浮动
前端·css·css3