useCallback()、useMemo() 解決了什么問題?

在閱讀本文之前,請確保您具有 js 基礎知識,知悉基礎數據類型與復雜數據類型的區別。
如果下面的代碼您不能理解,請略過此文以節約您的時間。

true === true       // true
false === false     // true
1 === 1             // true
'a' === 'a'         // true

{} === {}                 // false
[] === []                 // false
() => {} === () => {}     // false

目錄:

  • React.memo()
  • React.useCallback()
  • React.useMemo()

React.memo()

問題

React 中當組件的 props 或 state 變化時,會重新渲染視圖,實際開發會遇到不必要的渲染場景。看個例子:

子組件:

function ChildComp () {
  console.log('render child-comp ...')
  return <div>Child Comp ...</div>
}

父組件:

function ParentComp () {
  const [ count, setCount ] = useState(0)
  const increment = () => setCount(count + 1)

  return (
    <div>
      <button onClick={increment}>點擊次數:{count}</button>
      <ChildComp />
    </div>
  );
}

子組件中有條 console 語句,每當子組件被渲染時,都會在控制臺看到一條打印信息。

點擊父組件中按鈕,會修改 count 變量的值,進而導致父組件重新渲染,此時子組件壓根沒有任何變化(props、state),但在控制臺中仍然看到子組件被渲染的打印信息。

圖1

我們期待的結果:子組件的 props 和 state 沒有變化時,即便父組件渲染,也不要渲染子組件。

解決

修改子組件,用 React.memo() 包一層。

這種寫法是 React 的高階組件寫法,將組件作為函數(memo)的參數,函數的返回值(ChildComp)是一個新的組件。

import React, { memo } from 'react'

const ChildComp = memo(function () {
  console.log('render child-comp ...')
  return <div>Child Comp ...</div>
})

覺得上面??那種寫法別扭的,可以拆開寫。

import React, { memo } from 'react'

let ChildComp = function () {
  console.log('render child-comp ...')
  return <div>Child Comp ...</div>
}

ChildComp = memo(ChildComp)

此時再次點擊按鈕,可以看到控制臺沒有打印子組件被渲染的信息了。

(控制臺中打印的那一行值是第一次渲染父組件時,渲染子組件打印的,后面再點擊按鈕重新渲染父組件時,并沒有再重新渲染子組件)

圖2

React.useCallback()

問題

可別以為到這里就結束了!

上面的例子中,父組件只是簡單調用子組件,并未給子組件傳遞任何屬性。
看一個父組件給子組件傳遞屬性的例子:

子組件:(子組件仍然用 React.memo() 包裹一層)

import React, { memo } from 'react'

const ChildComp = memo(function ({ name, onClick }) {
  console.log('render child-comp ...')
  return <>
    <div>Child Comp ... {name}</div>
    <button onClick={() => onClick('hello')}>改變 name 值</button>
  </>
})

父組件:

function ParentComp () {
  const [ count, setCount ] = useState(0)
  const increment = () => setCount(count + 1)

  const [ name, setName ] = useState('hi~')
  const changeName = (newName) => setName(newName)  // 父組件渲染時會創建一個新的函數

  return (
    <div>
      <button onClick={increment}>點擊次數:{count}</button>
      <ChildComp name={name} onClick={changeName}/>
    </div>
  );
}

父組件在調用子組件時傳遞了 name 屬性和 onClick 屬性,此時點擊父組件的按鈕,可以看到控制臺中打印出子組件被渲染的信息。

圖1

React.memo() 失效了???

分析下原因:

  • 點擊父組件按鈕,改變了父組件中 count 變量值(父組件的 state 值),進而導致父組件重新渲染;
  • 父組件重新渲染時,會重新創建 changeName 函數,即傳給子組件的 onClick 屬性發生了變化,導致子組件渲染;

感覺一切又說的過去,由于子組件的 props 改變了,所以子組件渲染了,沒問題呀!

回過頭想一想,我們只是點擊了父組件的按鈕,并未對子組件做任何操作,壓根就不希望子組件的 props 有變化。

useCallback 鉤子進一步完善這個缺陷。

解決

修改父組件的 changeName 方法,用 useCallback 鉤子函數包裹一層。

import React, { useCallback } from 'react'

function ParentComp () {
  // ...
  const [ name, setName ] = useState('hi~')
  // 每次父組件渲染,返回的是同一個函數引用
  const changeName = useCallback((newName) => setName(newName), [])  

  return (
    <div>
      <button onClick={increment}>點擊次數:{count}</button>
      <ChildComp name={name} onClick={changeName}/>
    </div>
  );
}

此時點擊父組件按鈕,控制臺不會打印子組件被渲染的信息了。

究其原因:useCallback() 起到了緩存的作用,即便父組件渲染了,useCallback() 包裹的函數也不會重新生成,會返回上一次的函數引用。

React.useMemo()

問題

useMemo 又是干嘛的呢?

前面父組件調用子組件時傳遞的 name 屬性是個字符串,如果換成傳遞對象會怎樣?

下面例子中,父組件在調用子組件時傳遞 info 屬性,info 的值是個對象字面量,點擊父組件按鈕時,發現控制臺打印出子組件被渲染的信息。

import React, { useCallback } from 'react'

function ParentComp () {
  // ...
  const [ name, setName ] = useState('hi~')
  const [ age, setAge ] = useState(20)
  const changeName = useCallback((newName) => setName(newName), [])
  const info = { name, age }    // 復雜數據類型屬性

  return (
    <div>
      <button onClick={increment}>點擊次數:{count}</button>
      <ChildComp info={info} onClick={changeName}/>
    </div>
  );
}

分析原因跟調用函數是一樣的:

  • 點擊父組件按鈕,觸發父組件重新渲染;
  • 父組件渲染,const info = { name, age } 一行會重新生成一個新對象,導致傳遞給子組件的 info 屬性值變化,進而導致子組件重新渲染。

解決

使用 useMemo 對對象屬性包一層。

useMemo 有兩個參數:

  • 第一個參數是個函數,返回的對象指向同一個引用,不會創建新對象;
  • 第二個參數是個數組,只有數組中的變量改變時,第一個參數的函數才會返回一個新的對象。
function ParentComp () {
  // ....
  const [ name, setName ] = useState('hi~')
  const [ age, setAge ] = useState(20)
  const changeName = useCallback((newName) => setName(newName), [])
  const info = useMemo(() => ({ name, age }), [name, age])   // 包一層

  return (
    <div>
      <button onClick={increment}>點擊次數:{count}</button>
      <ChildComp info={info} onClick={changeName}/>
    </div>
  );
}

再次點擊父組件按鈕,控制臺中不再打印子組件被渲染的信息了。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,563評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,694評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,672評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,965評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,690評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,019評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,013評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,188評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,718評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,438評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,667評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,149評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,845評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,252評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,590評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,384評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容