講講react hooks里面的useCallback/useMemo

引言

自從react hooks出現(xiàn)以來(lái)像屋,越來(lái)越多的人或者團(tuán)隊(duì)選擇使用react hooks,很多人都覺(jué)得useCallback是解決性能問(wèn)題的一大利器,但你真的用對(duì)了么项炼?

下面就是筆者在實(shí)踐中得出在具體場(chǎng)景中如何使用好useCallback來(lái)提高性能的結(jié)論。

背景知識(shí)

說(shuō)起useCallback為什么可以解決性能問(wèn)題示绊,就涉及到re-render問(wèn)題了锭部,眾所周知在react中父組件的re-render會(huì)引發(fā)子組件的re-render,但有時(shí)候的re-render其實(shí)是不必要的耻台。例如:父組件并未傳遞props給子組件空免,渲染結(jié)果不變。

運(yùn)行以下案例可以發(fā)現(xiàn)input輸入內(nèi)容后盆耽,觸發(fā)setState蹋砚,從而觸發(fā)Case1組件的re-render,當(dāng)父組件re-render時(shí)摄杂,子組件A也會(huì)發(fā)生re-render坝咐。當(dāng)你每次輸入input內(nèi)容,都會(huì)在控制臺(tái)中看到有render_A的log析恢。

// case1
class A extends React.Component {
  // A 父組件的count變化時(shí)墨坚,A組件會(huì)不斷的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來(lái)解決子組件re-render的問(wèn)題

以上的案例說(shuō)明了新能浪費(fèi)的原因,那么要如何使用useCallback來(lái)解決子組件re-render的問(wèn)題呢映挂?

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個(gè)參數(shù)泽篮,第一個(gè)是inline的callback函數(shù),第二個(gè)是依賴項(xiàng)數(shù)組柑船。使用useCallback在依賴項(xiàng)發(fā)生變更時(shí)將會(huì)返回一個(gè)callback函數(shù)的memoized版本帽撑。當(dāng)你把callback函數(shù)傳遞給經(jīng)過(guò)子組件時(shí),如果使用了useCallback會(huì)因?yàn)閜rops的相等性而避免了非必要的渲染鞍时。

那么在實(shí)際使用中真的用對(duì)了方式么亏拉?

錯(cuò)誤使用案例1

看以下例子,子組件A的回調(diào)函數(shù)已經(jīng)使用了useCallback逆巍,但是當(dāng)你通過(guò)input改變count的值時(shí)及塘,子組件A還是在不斷的re-render。

// case2
const A = ({ onClick }) => {
  // A 父組件的count變化時(shí)锐极,A組件仍舊會(huì)不斷的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} />
    </>
  );
}

以上案例為什么沒(méi)有避免無(wú)效的re-render呢吮蛹?

是因?yàn)楹瘮?shù)式組件要避免re-render,還需要結(jié)合React.memo來(lái)使用敌蚜。使用高階組件React.memo來(lái)包裹函數(shù)式組件碗脊,它和類組件的PureComponent類似,也是對(duì)props進(jìn)行淺比較(根據(jù)內(nèi)存地址判斷)決定是否更新庇勃。

在函數(shù)組件中,函數(shù)作為props傳遞給子組件時(shí)槽驶,無(wú)論子組件是pureComponent還是用React.memo進(jìn)行包裹责嚷,都會(huì)讓子組件render,而配合useCallback使用就能讓子組件不隨父組件render掂铐。

上面案例修改一下罕拂,如下就不會(huì)發(fā)生re-render


const A = ({ onClick }) => {
  // A 父組件的count變化時(shí),A組件不會(huì)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要列清楚

為什么說(shuō)使用useCallback爆班,dependencies要列清楚呢,先來(lái)看以下案例:

錯(cuò)誤使用案例2

看下面的例子辱姨,在子組件B的回調(diào)函數(shù)中柿菩,使用了useCallback,但是沒(méi)有添加任何的dependencies雨涛,那么onClick useCallback回調(diào)函數(shù)中count的值永遠(yuǎn)都是初始值0枢舶。在input中改變了值后點(diǎn)擊A組件,在console中展示效果永遠(yuǎn)都是1替久。


const A = ({ onClick }) => {
  // A 父組件的count變化時(shí)凉泄,A組件不會(huì)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中改變了值后點(diǎn)擊A組件蚯根,console中輸出就是當(dāng)前count的最新值后众。所以在使用useCallback時(shí),一定要把當(dāng)前的回調(diào)函數(shù)的dependencies梳理清楚颅拦,避免值沒(méi)更新導(dǎo)致的bug蒂誉,例如分頁(yè)獲取數(shù)據(jù)的時(shí)候,永遠(yuǎn)獲取的是第一頁(yè)的數(shù)據(jù)等距帅。

【當(dāng)前案例只用來(lái)說(shuō)明dependencies正確的重要性】

const A = ({ onClick }) => {
  // A 父組件的count變化時(shí)右锨,A組件會(huì)不斷的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后,當(dāng)dependencies變化時(shí)會(huì)導(dǎo)致子組件隨著父組件re-render锥债。

所以在具體使用中陡蝇,如果導(dǎo)致父組件re-render的因素又同時(shí)全都是子組件useCallback的dependencies的話痊臭,就不必使用useCallback多此一舉了哮肚,反正都要跟著父組件一起render的。就像上面這個(gè)case一樣广匙。

如何從 useCallback 讀取一個(gè)經(jīng)常變化的值的方法可以查看官方文檔:英文版,中文版

如果觸發(fā)父組件的render因素很多允趟,但是觸發(fā)子組件的因素很少的話,就盡可能使用useCallback+React.memo來(lái)減少子組件的render次數(shù)鸦致。

【使用dependencies注意事項(xiàng)】 使用useEffect時(shí)潮剪,dependencies是非純函數(shù)涣楷,使用useCallback時(shí)要注意避免死循環(huán)。在實(shí)踐過(guò)程中最容易出現(xiàn)的一種死循環(huán)就是非純函數(shù)中請(qǐng)求了分頁(yè)的數(shù)據(jù)抗碰,set到State中狮斗,然后又把非純函數(shù)作為useEffect的dependencies,那么setState后re-render弧蝇,re-render導(dǎo)致的非純函數(shù)又是新的instance碳褒,作為依賴項(xiàng)就又會(huì)變調(diào)用,因此陷入死循環(huán)看疗。

useMemo

如果是組件中有復(fù)雜計(jì)算的function沙峻,應(yīng)該使用usememo而不是useCallback。因?yàn)閡seCallback緩存函數(shù)的引用两芳,useMemo緩存計(jì)算數(shù)據(jù)的值摔寨。useMemo是避免在每次渲染時(shí)都進(jìn)行高開(kāi)銷的計(jì)算的優(yōu)化的策略.

useMemo需要傳入兩個(gè)參數(shù),第一個(gè)參數(shù)是callback(回調(diào)函數(shù))怖辆,并把要邏輯處理函數(shù)放在callback內(nèi)執(zhí)行(該函數(shù)需要有返回值)是复,第二個(gè)參數(shù)是dependencies,和useCallback/useEffect一樣是引入的外部參數(shù)或者是依賴參數(shù)疗隶。

useMemo 返回一個(gè) memoized 值佑笋。在依賴參數(shù)不變的的情況返回的是上次第一次計(jì)算的值,當(dāng)依賴參數(shù)發(fā)生變化時(shí)useMemo就會(huì)自動(dòng)重新計(jì)算返回一個(gè)新的 memoized值斑鼻。

使用案例如下:

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

在a和b的變量值不變的情況下蒋纬,memoizedValue的值不變。即:useMemo函數(shù)的第一個(gè)入?yún)⒑瘮?shù)不會(huì)被執(zhí)行坚弱,從而達(dá)到節(jié)省計(jì)算量的目的蜀备。

結(jié)尾

以上就是關(guān)于useCallback和useMemo的具體使用方式,也通過(guò)案例解釋了為什么使用這兩者可以達(dá)到性能優(yōu)化的目的荒叶。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末碾阁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子些楣,更是在濱河造成了極大的恐慌脂凶,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件愁茁,死亡現(xiàn)場(chǎng)離奇詭異蚕钦,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)鹅很,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門嘶居,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人促煮,你說(shuō)我怎么就攤上這事邮屁≌” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵佑吝,是天一觀的道長(zhǎng)坐昙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)芋忿,這世上最難降的妖魔是什么民珍? 我笑而不...
    開(kāi)封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮盗飒,結(jié)果婚禮上嚷量,老公的妹妹穿的比我還像新娘。我一直安慰自己逆趣,他們只是感情好蝶溶,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著宣渗,像睡著了一般抖所。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上痕囱,一...
    開(kāi)封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天田轧,我揣著相機(jī)與錄音,去河邊找鬼鞍恢。 笑死傻粘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的帮掉。 我是一名探鬼主播弦悉,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼蟆炊!你這毒婦竟也來(lái)了稽莉?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤涩搓,失蹤者是張志新(化名)和其女友劉穎污秆,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體昧甘,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡良拼,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了疾层。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片将饺。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贡避,死狀恐怖痛黎,靈堂內(nèi)的尸體忽然破棺而出予弧,到底是詐尸還是另有隱情,我是刑警寧澤湖饱,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布掖蛤,位于F島的核電站,受9級(jí)特大地震影響井厌,放射性物質(zhì)發(fā)生泄漏蚓庭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一仅仆、第九天 我趴在偏房一處隱蔽的房頂上張望器赞。 院中可真熱鬧,春花似錦墓拜、人聲如沸港柜。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)夏醉。三九已至,卻和暖如春涌韩,著一層夾襖步出監(jiān)牢的瞬間畔柔,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工臣樱, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留靶擦,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓雇毫,卻偏偏與公主長(zhǎng)得像奢啥,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子嘴拢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355