React Hook - 如何使用useMemo和useCallback

Hook是在React 16.8之后增加的一项新功能,能够帮助我们在不写class的情况下使用state和其他React的相关特性。关于如何使用Hook官网有很多介绍,但理论是一码事,实践又是另一码事。

如果写过一段时间的React,我们知道通过使用 useMemouseCallback 能够帮助我们的代码提高性能,避免component在re-render时的无效计算。于是,慢慢的大家会发现前端代码里到处都是 useMemouseCallback,不仅影响代码的可读性,而且也不利于debug。

本文会通过举例等方式告诉大家,其实我们前端工程中几乎90%的 useMemouseCallback 都是可以移除的,而且代码不会有任何问题,且启动加载速度会更快。

为什么需要 useMemo和 useCallback

为了在re-render之间能够进行缓存。如果hook包裹了某个值或者方法,react会在初始渲染时对其进行缓存,并在多次渲染时对该保存值进行引用。如果没有hook,非原生类型如数组、对象和方法会在每次重新渲染时重新创建。例如:

const a = { "test": 1 };
const b = { "test": 1'};

console.log(a === b); // will be false

const c = a; // "c" is just a reference to "a"

console.log(a === c); // will be true

另一个更贴近react代码的例子:

const Component = () => {
  const a = { test: 1 };

  useEffect(() => {
    // "a" will be compared between re-renders
  }, [a]);

  // the rest of the code
};

auseEffect的一个依赖值,React每次重新渲染 Component 时都会与其之前的值进行对比。a是在 Component 中定义的一个对象,因此每次重新渲染都会重新创建。因此“渲染前”的a与“渲染后”的 a比较结果为不想等,因此 useEffect 也会在每次重新渲染时被触发。
为了避免上述情况,我们可以对a使用 useMemo

const Component = () => {
  // preserving "a" reference between re-renders
  const a = useMemo(() => ({ test: 1 }), []);

  useEffect(() => {
    // this will be triggered only when "a" value actually changes
  }, [a]);

  // the rest of the code
};

现在只有当a的值真的发生变化时才会触发useEffect
useCallback的使用与上述类似,只是它更常用于方法:

const Component = () => {
  // preserving onClick function between re-renders
  const fetch = useCallback(() => {
    console.log('fetch some data here');
  }, []);

  useEffect(() => {
    // this will be triggered only when "fetch" value actually changes
    fetch();
  }, [fetch]);

  // the rest of the code
};

这里要注意,useMemouseCallback只有在重新渲染期间才有用,在初始渲染时,他们的使用会导致react做更多的事情,所以程序也会更慢一些。如果你的代码到处都是用了成百上千个hook,这种减速甚至是肉眼可感知的。

为什么Component会重新渲染

两种场景:

  1. 当Component的state或者prop发生变化时,React会重新渲染该Component
  2. 当父Component被重新渲染时
    即当一个Component重新渲染它自己时,它也会重新渲染它所有的子Component,例如:
const App = () => {
  const [state, setState] = useState(1);

  return (
    <div className="App">
      <button onClick={() => setState(state + 1)}> click to re-render {state}</button>
      <br />
      <Page />
    </div>
  );
};

App Component有一些state也有一些自Component,例如 Page,当点击页面上的button,state会发生改变,因此会触发 App的重新渲染,因此会触发其所有子Component的重新渲染,包括 Page,即使他没有props。
而如果在 Page 内部,还有一些其他子Component:

const Page = () => <Item />;

即使该子Component没有state也没有props,但也会由于App的重新渲染而被触发重新渲染。由此得出结论,App由于其state变化而触发的重新渲染,会触发整个程序的重新渲染链。
如何中断这条重新渲染链呢?对子Component进行缓存:

  1. 使用 useMemo hook
  2. 使用 React.memo 工具
    只有通过上面两种方法,React才会在重新渲染之前停止并检查其props值是否改变:
const Page = () => <Item />;
const PageMemoized = React.memo(Page);

const App = () => {
  const [state, setState] = useState(1);

  return (
    ... // same code as before
      <PageMemoized />
  );
};

有且只有在上述情况下,讨论props是否被缓存才有意义。

例如:

const App = () => {
  const [state, setState] = useState(1);
  const onClick = () => {
    console.log('Do something on click');
  };
  return (
    // page will re-render regardless of whether onClick is memoized or not
    <Page onClick={onClick} />
  );
};

如果 Page没有被缓存,当 App 重新渲染时,React发现 Page是其子Component,就会也重新渲染它。那么不论 onClick 是否使用useCallback都是没有意义的;

对上述例子进一步优化,缓存 Page:

const PageMemoized = React.memo(Page);

const App = () => {
  const [state, setState] = useState(1);
  const onClick = () => {
    console.log('Do something on click');
  };
  return (
    // PageMemoized WILL re-render because onClick is not memoized
    <PageMemoized onClick={onClick} />
  );
};

如果 Page被缓存,当 App 重新渲染时,React发现 PageMemoized是其子Component且已使用 React.memo,因此中断重新渲染链条,首先检查 PageMemoized 的 props 是否发生变化。上述例子中,由于 onClick 没有被缓存,所以props发生变化,PageMemoized 会被重新渲染;

对上述例子继续优化,缓存 Page,使用 useCallback :

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} />
  );
};

那么,React在PageMemoized 上停止重新渲染链并检查其props,发现 onClick没有发生变化,因此PageMemoized 也不会被重新渲染;

对上述例子继续变种,在PageMemoized 上增加另一个没有缓存的值:

const PageMemoized = React.memo(Page);

const App = () => {
  const [state, setState] = useState(1);
  const onClick = useCallback(() => {
    console.log('Do something on click');
  }, []);

  return (
    // page WILL re-render because value is not memoized
    <PageMemoized onClick={onClick} value={[1, 2, 3]} />
  );
};

那么,React在PageMemoized 上停止重新渲染链并检查其props,发现 onClick没有发生变化,但是value发生变化,因此PageMemoized 会被重新渲染;

综上所述,得出结论:只有Component本身和它的每一个props都被缓存时,hook的优化才有意义。否则都是对内存的浪费,且会降低代码的可读性。

参考文章

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

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

推荐阅读更多精彩内容

  • 因为篇幅原因,React hook的由来和影响这里不做介绍。本文主要介绍的是hook的基本API,还有从class...
    Yong_bcf4阅读 1,402评论 0 2
  • React Hook是React函数式组件,它不仅仅有函数组件的特性,还带有React框架的特性。所以,官网文档多...
    娜姐聊前端阅读 439评论 3 1
  • 要点: 可以在不编写class的情况下使用 state 以及其他react特性。 Hook没有破坏性改动, 完全是...
    koala949阅读 854评论 0 0
  • useState useState 返回的第一个值将始终是更新后最新的 state,并且与 class 组件中的 ...
    三粒黑子球阅读 451评论 0 0
  • 一、组件类 React的核心是组件, 在v16.8之前,组件的标准写法是类(class)。 以下为一个简单的组件类...
    郭_小青阅读 686评论 1 5