useMemo & useCallback 使用指南

179 阅读3分钟

目前存在的问题

很多人在写函数组件(FC)时使用useMemo & useCallback不求甚解,好像在创建方法时只要使用useCallback wrap(声明变量使用useMemo wrap)就能达到性能优化的玄学,导致多数情况下useCallback & useMemo在代码里存在的原因仅仅是因为“大家都在用”。

如果不理解为什么我们要使用它,最好不要用它。

需要记住的一点是,useMemo & useCallback 只在re-render时期有效。在初始化渲染( initial render)时期, 不仅无用而且有害,它会使React做一些不必要的工作。这意味着在初始化渲染时期,使用useMemo & useCallback比不使用还要慢一点点🤏。但是如果整个工程有成千上万的地方使用,就能明显感知到这种性能影响。这两个hooks的作用就是为了实现渲染优化,如果大量错误地使用,反而与优化的初衷背道而驰。

前置知识:

首先我们需要知道导致React组件re-render的唯二两种情况:

  1. props 或者states 改变(浅比较);

  2. 父组件重新渲染。

以及需要了解基本数据类型与引用数据类型:

'1' === '1' // true
[] === [] // false
{'1'} === {'1'} // false

还有一些hooks使用的基本规则:

  • 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用;

  • 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用。

为什么需要useMemo & useCallback?

在每个re-render之间缓存值,避免在每次渲染时都进行高开销的计算,防止子组件进行不必要的渲染。

你是否是这样使用useMemo & useCallback的:

  • 用useCallback包裹onClick防止重新渲染(一)

    const Component = () => {
      const onClick = useCallback(() => {
       /* do something */
      }, []);
      return (
       <>
         <button onClick={onClick}>Click me</button>
         ... // some other components
       </>
      );
    };
    
  • 用useCallback包裹onClick防止重新渲染(二)

    const Item = ({ item, onClick, value }) => <button onClick={onClick}>{item.name}</button>;
    const Component = ({ data }) => {
      const value = { a: someStateValue };
      const onClick = useCallback(() => {
       /* do something on click */
      }, []);
      return (
       <>
         {data.map((d) => (
           <Item item={d} onClick={onClick} value={value} />
         ))}
       </>
      );
    };
    
  • 用useMemo包裹value防止重新创建onClick

    const Item = ({ item, onClick }) => <button onClick={onClick}>{item.name}</button>;
    const Component = ({ data }) => {
      const value = useMemo(() => ({ a: someStateValue }), [someStateValue]);
      const onClick = useCallback(() => {
       console.log(value);
      }, [value]);
      return (
       <>
         {data.map((d) => (
           <Item item={d} onClick={onClick} />
         ))}
       </>
      );
    };
    

如果你一直是这样使用useMemo & useCallback,那么恭喜你!已经完美掌握了useMemo & useCallback无效的使用方法

如何正确有效使用useMemo & useCallback:

useMemo

pre-vuer可以将它理解为vue中的computed

依赖项为空则每次re-render都会触发

useMemo 和 useEffect 很相似,但是执行时机不同

useCallback

有且只有一种有效的情况:useCallback需要配合React.memo使用,即当组件的所有props和组件本身都被memoized。(其中如果有一个prop没有被memoized都是无效的)

  • 以下是正确使用姿势的代码示例

    const PageMemoized = React.memo(Page);
    const App = () => {
      const [state, setState] = useState(1);
      const onClick = useCallback(() => {
       console.log('Do something on click');
      }, []);
      return (
       // PageMemoized will NOT re-render because onClick is memoized
       <PageMemoized onClick={onClick} />
       // page WILL re-render because value is not memoized
       // <PageMemoized onClick={onClick} value={[1, 2, 3]} />
       // again 其中如果有一个prop没有被memoized都是无效的
      );
    };
    useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
    

什么情况下使用?

useMemo:当需要缓存高开销计算的值
  • 使用时需权衡:re-render不一定会发生,initial render一定会发生;
useCallback:防止子组件进行不必要的渲染,配合React.memo使用

后记

基于react组件更新的特性,部分开发者使用极端优化策略,秉承“宁可错杀一千,不肯放过一个”原则,memoize 所有的组件和 props,防止有“漏网之鱼”(即忘记被memoized的prop),导致整个memo失效。我不是很认同这个激进的优化方案,首先因为代码是人写的,总会有疏忽的时候,很难做到完美,而且也违背了克努特优化原则。

“过早优化是万恶之源” --- Tony Hoare

May this day be your last day in useMemo and useCallback hell!

相关阅读推荐

How to useMemo and useCallback: you can remove most of them

Why We Memo All the Things

useMemo..一把梭?达咩!✋|一文告诉你为什么React不把他们设为默认方法