memo的问题

使用memo方法包裹组件,可以让组件不因为父组件的刷新而刷新,但是如果接收到了一个对象类型的props,这个memo就不能正常工作了

  1. import { useState, memo } from "react";
  2. function App() {
  3. const [count, setCount] = useState("");
  4. const obj = () => {
  5. return null;
  6. };
  7. return (
  8. <>
  9. <button onClick={() => setCount(count + 1)}>add inc</button>
  10. <div>{count}</div>
  11. <CompChildren />
  12. <CompChildrenMemo name="wuzhao" />
  13. <CompChildrenMemoPropsObj fn={obj} />
  14. </>
  15. );
  16. }
  17. function CompChildren() {
  18. console.log("CompChildren render");
  19. return <div>CompChildren</div>;
  20. }
  21. const CompChildrenMemo = memo(() => {
  22. console.log("CompChildrenMemo render");
  23. return <div>CompChildrenMemo</div>;
  24. });
  25. const CompChildrenMemoPropsObj = memo(() => {
  26. console.log("CompChildrenMemoPropsObj render");
  27. return <div>CompChildrenMemoPropsObj</div>;
  28. });
  29. export default App;

然而,React.memo 是用 shallowly compare 的方法确认 props 的值是否一样, shallowly compare 在 props 是 Number 或 String 比较的是值,当 props 是 Object 是,比較的是引用 (reference)。
因此,当父元件重新渲染时,在父元件宣告的 Object 都会被重新分配引用,所以想要利用 React.memo 防止重新渲染就会失效。

React.memo 提供了第二个参数

要解决这个问题的方法有两种,第一种是 React.memo 提供了第二个参数,让我们可以自订比较 props 的方法,让 Object 不再只是比较记忆体位置。

const CompChildrenMemoPropsObj = memo(
  () => {
    console.log("CompChildrenMemoPropsObj render");
    return <div>CompChildrenMemoPropsObj</div>;
  },
  (preProps, nextProps) => {
    if (_.isEqual(preProps, nextProps)) return false;
  }
);

第二种方法则是 React.useCallback,让 React 可以自动记住 Object 的索引,解决 shallowly compare 的比较问题。
接下来,我们就来看看 React.useCallback。

React.useCallback 父组件上包裹

当父元件传递的 props 是 Object 时,父元件的状态被改变触发重新渲染,Object 的记忆体位址也会被重新分配。React.memo 会用 shallowly compare 比较 props 中 Object 的记忆体位址,这个比较方式会让子元件被重新渲染。
因此,React 提供了 React.useCallback 这个方法让 React 在元件重新渲染时,如果 dependencies array 中的值在没有被修改的情况下,它会帮我们记住 Object,防止 Object 被重新分配地址。

在父组件传递给子组件的方法上用useCallback包裹

  const obj = useCallback(() => {
    return null;
  }, []);

useMemo

有一种情况是与父组件无关,是子组件内无关的状态变更导致整个组件的函数重新执行的问题,
下边是增加了inc这个没有用到的变量,但还是导致了factorial重新执行;
还有一种情况是这个子组件状态没有变更,但由于父组件的状态更新而刷新了,那这个组件里的useMemo包裹的方法也是不会执行的。

import { useState, useMemo } from "react";
export default function CalculateFactorial() {
  const [number, setNumber] = useState(4);
  const [inc, setInc] = useState(0);
  // const factorial = factorialOf(number);
  const factorial = useMemo(() => factorialOf(number), [number]);
  const onChange = (event) => {
    setNumber(Number(event.target.value));
  };
  const onClick = () => setInc((i) => i + 1);

  return (
    <div>
      Factorial of
      <input type="number" value={number} onChange={onChange} />
      is {factorial}
      <button onClick={onClick}>重新渲染</button>
    </div>
  );
}
function factorialOf(n) {
  console.log("factorialOf(n) called!");
  return n <= 0 ? 1 : n * factorialOf(n - 1);
}

summary

memo可以缓存组件防止被重复更新,但如果父组件传递了obj类型的props,则memo会失效,基本类型的props,memo还是可以正常工作。
解决memo接收obj类型的props的方式有两种,一种是memo的第二个参数,自定义比较器。一种是在父组件上把传递给子组件的引用使用useCallback包裹
useMemo与useCallback类似,不过useMemo缓存的是传入函数的执行结果。

https://medium.com/%E6%89%8B%E5%AF%AB%E7%AD%86%E8%A8%98/react-optimize-performance-using-memo-usecallback-usememo-a76b6b272df3

https://dmitripavlutin.com/react-usememo-hook/