React Hooks之useCallback、 useMemo

3,899 阅读6分钟

引言

自从react hooks出现以来,越来越多的人或者团队选择使用react hooks,很多人都觉得useCallback是解决性能问题的一大利器,但你真的用对了么?

下面就是笔者在实践中得出在具体场景中如何使用好useCallback来提高性能的结论。

背景知识

说起useCallback为什么可以解决性能问题,就涉及到re-render问题了,众所周知在react中父组件的re-render会引发子组件的re-render,但有时候的re-render其实是不必要的。例如:父组件并未传递props给子组件,渲染结果不变。

运行以下案例可以发现input输入内容后,触发setState,从而触发Case1组件的re-render,当父组件re-render时,子组件A也会发生re-render。当你每次输入input内容,都会在控制台中看到有render_A的log。

// case1
class A extends React.Component {
  // A 父组件的count变化时,A组件会不断的re-render
  render() {
    console.log("render_A");
    return <div>这是A组件</div>;
  }
}

export default function Case1() {
  const [count, setCount] = useState(0);

  const onChange = (data) => {
    setCount(data.target.value);
  };

  return (
    <>
      <input value={count} onChange={onChange} />
      <A />
    </>
  );
}

useCallback

如何使用useCallback来解决子组件re-render的问题

以上的案例说明了新能浪费的原因,那么要如何使用useCallback来解决子组件re-render的问题呢?

useCallback在官方文档是这么解释的:

Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).

useCallback有2个参数,第一个是inline的callback函数,第二个是依赖项数组。使用useCallback在依赖项发生变更时将会返回一个callback函数的memoized版本。当你把callback函数传递给经过子组件时,如果使用了useCallback会因为props的相等性而避免了非必要的渲染。

那么在实际使用中真的用对了方式么?

错误使用案例1

看以下例子,子组件A的回调函数已经使用了useCallback,但是当你通过input改变count的值时,子组件A还是在不断的re-render。

// case2
const A = ({ onClick }) => {
  // A 父组件的count变化时,A组件仍旧会不断的re-render
  console.log("case2: render_A");
  return <button onClick={onClick}>A组件+count</button>;
};

export default function Case2() {
  const [count, setCount] = useState(0);
  
  const onClick = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  return (
    <>
      <p>count:{count}</p>
      <A onClick={onClick} />
    </>
  );
}

以上案例为什么没有避免无效的re-render呢?

是因为函数式组件要避免re-render,还需要结合React.memo来使用。使用高阶组件React.memo来包裹函数式组件,它和类组件的PureComponent类似,也是对props进行浅比较(根据内存地址判断)决定是否更新。

在函数组件中,函数作为props传递给子组件时,无论子组件是pureComponent还是用React.memo进行包裹,都会让子组件render,而配合useCallback使用就能让子组件不随父组件render。

上面案例修改一下,如下就不会发生re-render


const A = ({ onClick }) => {
  // A 父组件的count变化时,A组件不会re-render
  console.log("case2: render_A");
  return <button onClick={onClick}>A组件+count</button>;
};
const B = React.memo(A);
export default function Case2() {
  const [count, setCount] = useState(0);
  
  const onClick = useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  return (
    <>
      <p>count:{count}</p>
      <B onClick={onClick} />
    </>
  );
}

使用useCallback,dependencies要列清楚

为什么说使用useCallback,dependencies要列清楚呢,先来看以下案例:

错误使用案例2

看下面的例子,在子组件B的回调函数中,使用了useCallback,但是没有添加任何的dependencies,那么onClick useCallback回调函数中count的值永远都是初始值0。在input中改变了值后点击A组件,在console中展示效果永远都是1。


const A = ({ onClick }) => {
  // A 父组件的count变化时,A组件不会re-render
  console.log("case2: render_A");
  return <button onClick={onClick}>A组件+count</button>;
};
const B = React.memo(A);
export default function Case2() {
  const [count, setCount] = useState(0);
 
  const onClick = useCallback(() => {
   console.log(count + 1); // 此处的count一直都是0
  }, []);

  return (
    <>
      <p>count:{count}</p>
      <B onClick={onClick} />
    </>
  );
}

把count作为dependencies加到useCallback中,在input中改变了值后点击A组件,console中输出就是当前count的最新值。所以在使用useCallback时,一定要把当前的回调函数的dependencies梳理清楚,避免值没更新导致的bug,例如分页获取数据的时候,永远获取的是第一页的数据等。

【当前案例只用来说明dependencies正确的重要性】

const A = ({ onClick }) => {
  // A 父组件的count变化时,A组件会不断的re-render
  console.log("case2: render_A");
  return <button onClick={onClick}>A组件+count</button>;
};
const B = React.memo(A);
export default function Case2() {
  const [count, setCount] = useState(0);
  const onChange = (data) => {
    setCount(data.target.value);
  };
  const onClick = useCallback(() => {
    console.log(count + 1);
  }, [count]);

  return (
    <>
      <p>count:{count}</p>
      <input value={count} onChange={onChange} /> 
      <B onClick={onClick} />
    </>
  );
}

添加dependencies后,当dependencies变化时会导致子组件随着父组件re-render。

所以在具体使用中,如果导致父组件re-render的因素又同时全都是子组件useCallback的dependencies的话,就不必使用useCallback多此一举了,反正都要跟着父组件一起render的。就像上面这个case一样。

如何从 useCallback 读取一个经常变化的值的方法可以查看官方文档:英文版,中文版

如果触发父组件的render因素很多,但是触发子组件的因素很少的话,就尽可能使用useCallback+React.memo来减少子组件的render次数。

【使用dependencies注意事项】 使用useEffect时,dependencies是非纯函数,使用useCallback时要注意避免死循环。在实践过程中最容易出现的一种死循环就是非纯函数中请求了分页的数据,set到State中,然后又把非纯函数作为useEffect的dependencies,那么setState后re-render,re-render导致的非纯函数又是新的instance,作为依赖项就又会变调用,因此陷入死循环。

useMemo

如果是组件中有复杂计算的function,应该使用usememo而不是useCallback。因为useCallback缓存函数的引用,useMemo缓存计算数据的值。useMemo是避免在每次渲染时都进行高开销的计算的优化的策略.

useMemo需要传入两个参数,第一个参数是callback(回调函数),并把要逻辑处理函数放在callback内执行(该函数需要有返回值),第二个参数是dependencies,和useCallback/useEffect一样是引入的外部参数或者是依赖参数。

useMemo 返回一个 memoized 值。在依赖参数不变的的情况返回的是上次第一次计算的值,当依赖参数发生变化时useMemo就会自动重新计算返回一个新的 memoized值。

使用案例如下:

const memoizedValue = useMemo(() => calculateFunc(a, b), [a, b]);

在a和b的变量值不变的情况下,memoizedValue的值不变。即:useMemo函数的第一个入参函数不会被执行,从而达到节省计算量的目的。

结尾

以上就是关于useCallback和useMemo的具体使用方式,也通过案例解释了为什么使用这两者可以达到性能优化的目的。