8.useMemo&useCallback

10/13/2023 react

# useMemo

来缓存每次重新渲染都需要计算的结果

const cachedValue = useMemo(calculateValue, dependencies)

import { useMemo } from 'react';
function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}
1
2
3
4
5
6
7
8
9
10

# useCallback

useCallback 是一个允许你在多次渲染中缓存函数的 React Hook

const cachedFn = useCallback(fn, dependencies)

import { useCallback } from 'react';
export default function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);
1
2
3
4
5
6
7
8
9
10

# 注意

useMemo, useCallbck 是 react hooks 中用来缓存数据的重要手段,使用这两个 hook 可以一定程度上的优化组件的性能。

很多同学倾向于给每个变量都套上 useMemo / useCallback ,不过这种方式是不对的,因为这种优化方式是有成本的。

每个 useMemo / useCallback 都是天然的闭包, 里面的垃圾数据得不到及时的释放,不合理的使用会造成数据堆积,变成负优化

# 什么时候该去使用useMemo捏

# 1.当自身是引用类型且要作为其他hook的依赖时

export const Component: React.FC = () => {
  const [someDatas, setSomeDatas] = useState([1, 2, 3]);
  const [otherData, setOtherData] = useState<{ bool: boolean }>({ bool: true });
  const datas100 = someDatas.map((item) => {
    return item + 100;
  });
  const { bool } = otherData;
  // Effect 1
  useEffect(() => {
    console.log('Effect1 : ', datas100);
  }, [datas100]);

  // Effect 2
  useEffect(() => {
    console.log('Effect2 : ', bool);
  }, [bool]);

  return (
    <div>
      <button
        onClick={() => {
          setSomeDatas((draft) => {
            return [...draft, 1];
          });
        }}
      >
        update someDatas
      </button>
      <button
        onClick={() => {
          setOtherData((draft) => {
            return { bool: !draft.bool };
          });
        }}
      >
        update otherDatas
      </button>
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

如果点击 update someDatas , 会更新 someDatas,从而引起 datas100 数据的变化,从而导致 Effect1 的重新执行,这没问题:打印:Effect1 : [101, 102, 103, 101]

如果点击 update otherDatas ,期望的是更新 otherDatas,从而引起 bool 数据的变化,从而引起 Effect2 的重新执行,但是…… 打印:Effect1 : [101, 102, 103, 101], Effect2 : false

在点击update otherDatas后,Effect1和Effect2被同时执行了!

原因: 当useState的数据改变时,会重新render,重新执行整个函数体。此时datas100和bool这两个变量将被重新计算生成。

但 react 的 hook(useEffect, useMemo, useCallback 等) 会有一个 deps array(第二个参数),而这些 hook 要不要再次被计算取决于本次 deps array 中的数据与上次 deps array 中的数据进行一次浅 diff,如果不相等则再次运行这个 hook,反之则不执行。

我们都知道在 js 在存储变量时,简单来说会把引用类型的地址存在数据中,而将存在数据中,而浅diff的原理是比较二者在栈中存储的数据是否相同,并不关心堆中的情况

由于在每次 render 中, datas100 都会被重新计算生成,因此每次 data100 在栈中存储的地址都是新分配的,即使在堆中的值相同,但是在 deps array 看来,当前状态和上一个状态的 datas100 就是不同的,因此每次 render 都会重新执行 Effect1。

虽然每次 render 中,bool 这个变量都会被重新获取,但因为 bool 是一个简单类型,值直接存在栈中,虽然每次都被重新生成,不过只要他们的值相同, deps array 就会认为他们是相等的,所以只会在 bool 的值本身改变后执行 Effect2

对上面的组件进行一次改进:

export const Component: React.FC = () => {
  const [someDatas, setSomeDatas] = useState([1, 2, 3]);
  const [otherData, setOtherData] = useState<{ bool: boolean }>({ bool: true });
  const datas100 = useMemo(
    () =>
      someDatas.map((item) => {
        return item + 100;
      }),
    [someDatas],
  );
  const { bool } = otherData;
  // Effect 1
  useEffect(() => {
    console.log('Effect1 : ', datas100);
  }, [datas100]);

  // Effect 2
  useEffect(() => {
    console.log('Effect2 : ', bool);
  }, [bool]);

  return (
    <div>
      <button
        onClick={() => {
          setSomeDatas((draft) => {
            return [...draft, 1];
          });
        }}
      >
        update someDatas
      </button>
      <button
        onClick={() => {
          setOtherData((draft) => {
            return { bool: !draft.bool };
          });
        }}
      >
        update otherDatas
      </button>
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

改动核心:使用useMemo将获取datas100的逻辑包裹起来,这样只要someDatas不更新,datas就不会更新。那么Effect1将不会每次都执行。

点击update otherDatas:打印:Effect2:false。成功了,只有Effect2被执行了,再尝试点击update someDatas:Effect1 : [101, 102, 103, 101]。可以得出结论:当自身是引用类型,且要作为其他 hook 的依赖时,需要包裹 useMemo

# 2.当数据为引用类型,且要作为props传递给子组件时,推荐用useMemo包裹

interface Props {
  datas100: number[];
  bool: boolean;
}

export const ChildComponent: React.FC<Props> = ({ datas100, bool }) => {
  // Effect 1
  useEffect(() => {
    console.log('Effect1 : ', datas100);
  }, [datas100]);

  // Effect 2
  useEffect(() => {
    console.log('Effect2 : ', bool);
  }, [bool]);
  return <div>我是子组件</div>;
};

export const Component: React.FC = () => {
  const [someDatas, setSomeDatas] = useState([1, 2, 3]);
  const [otherData, setOtherData] = useState<{ bool: boolean }>({ bool: true });
  const datas100 = someDatas.map((item) => {
    return item + 100;
  });
  const { bool } = otherData;

  return (
    <div>
      <button
        onClick={() => {
          setSomeDatas((draft) => {
            return [...draft, 1];
          });
        }}
      >
        update someDatas
      </button>
      <button
        onClick={() => {
          setOtherData((draft) => {
            return { bool: !draft.bool };
          });
        }}
      >
        update otherDatas
      </button>
      <ChildComponent datas100={datas100} bool={bool} />
    </div>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

点击update otherDatas,打印如下:

Effect1 : [101, 102, 103]
Effect2 :  false
1
2

Effect1 又被错误的执行了,原因很简单,看了上文的大家已经懂了,这里不做过多阐述。

这个例子就是典型的,将一个引用类型传递给了子组件,但是没有做缓存。

如果子组件的某个 hook 以这个变量做了依赖,那这个 hook 也就失效了,他就像病毒一样,如果这个 hook 是 useMemo ,且他被传递给了更深一层的组件,那可想而知,后果很严重。

所以我通常给会传递给子组件的引用类型都加上 useMemo ,可能这个做法会让很多人反感,但毕竟业务代码不只有一个人维护,其他人也可能会更新这部分代码,如果他在子组件中使用了某个没有做缓存的引用类型的 props,那岂不原地裂开?

# 3.当处理数据的时间复杂度较高时,应当用useMemo包裹

interface Props {
  datas: number[];
  anyProps: any;
}

const Component: React.FC<Props> = ({ datas }) => {
  const str = datas.sort((a, b) => a > b ? 1 : -1).join('->');
  return <div>{str}</div>;
};
1
2
3
4
5
6
7
8
9

在这个 demo 中,假设 datas 的 length 非常大,那 datas.sort 的计算量会变得比较恐怖,每次 Component 更新(不是 datas 自身引起的更新)都会引起 str 被重新计算,这明显是不符合预期的,并且会非常卡顿。

因此我们可以给 str 也包裹上 useMemo:

const Component: React.FC<Props> = ({ datas }) => {
  const str = useMemo(
    () => datas.sort((a, b) => (a > b ? 1 : -1)).join('->'),
    [datas],
  );
  return <div>{str}</div>;
};
1
2
3
4
5
6
7

Component的更新不是由datas引起的,str就不会被重新计算,从而提高性能。

# 4.当你不知道应不应该加 useMemo,且它恰好是引用类型时

警告:最好不要这么做,除非你不熟悉react

可以想象一下,如果所有的引用类型都加了 useMemo,就绝不会出现 deps arr diff 混乱的问题,这种方式只能保证逻辑不会出错,但是性能就不一定了……

# 什么时候该去使用useCallback捏

useCallback其实是memo一个方法的useMemo的简写版本

useMemo(() => {
  return () => {
    console.log("do something");
  };
}, []);
1
2
3
4
5

简化成

useCallback(() => {
  console.log("do something");
}, []);
1
2
3

那我们该如何判断一个 function 是否需要被包裹 useCallback 呢,其实我们只要遵循 上文中 useMemo 返回 引用类型时的使用原则即可:

最近更新时间: 10/13/2023, 7:57:32 AM
강남역 4번 출구
Plastic / Fallin` Dild