讲讲react hooks里面的useCallback/useMemo

引言

自从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的具体使用方式,也通过案例解释了为什么使用这两者可以达到性能优化的目的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343