不要再濫用useMemo了!你應該從新思考Hooks memoization

不要再濫用useMemo了!你應該從新思考Hooks memoization


image.png




做者 | Ohans Emmanuel譯者 | 王強編輯 |  Yonie在使用 React Hooks 的過程當中,做者發現過渡頻繁的應用 useMemo 用會影響程序的性能。在本文中做者將與你們分享如何避免過分使用 useMemo,使程序遠離性能問題。前端

通過觀察總結,我發如今兩種狀況下 useMemo 要麼沒什麼用,要麼就是用得太多了,並且可能會影響應用程序的性能表現。react

第一種狀況很容易就能推斷出來,可是第二種狀況就比較隱蔽了,很容易被忽略。若是你在生產環境的應用程序中使用了 Hook,那麼你就可能會在這兩個場景中使用 useMemo Hook。git

下面我就會談一談爲何這些 useMemo 沒什麼必要,甚至可能影響你的應用性能。此外我會教你們在這些場景中避免過分使用 useMemo 的方法。github

咱們開始吧。小程序

不須要 useMemo 的狀況

爲了方便,咱們把這兩類場景分別稱爲獅子和變色龍。image.png前端工程化

先不用糾結爲何這麼叫,繼續讀下去就是。數組

當你撞上一頭雄獅,你的第一反應就是撒丫子跑,不要成爲獅子的盤中餐,而後活下來跟別人吹牛。這時候可沒空思考那麼多。安全

這就是場景 A。它們是獅子,你應該下意識地躲開它們。前端框架

但在談論它們以前,咱們先來看看更隱蔽的變色龍場景。微信

相同的引用和開銷不大的操做

參考下面的示例組件:

/**
 @param {number} page
 @param {string} type
**/
const myComponent({page, type}) {
 const resolvedValue = useMemo(() => {
    getResolvedValue(page, type)
 }, [page, type])

 return <ExpensiveComponent resolvedValue={resolvedValue}/>
}

如上所示,顯然做者使用了 useMemo。這裏他們的思路是,當對 resolvedValue 的引用出現更改時,他們不想從新渲染 ExpensiveComponent。

雖然說這個擔心是正確的,但不管什麼時候要用 useMemo 以前都應該考慮兩個問題:
  • 首先,傳遞給 useMemo 的函數開銷大不大?在上面這個示例中就是要考慮 getResolvedValue 的開銷大不大?JavaScript 數據類型的大多數方法都是優化過的,例如 Array.map、Object.getOwnPropertyNames() 等。若是你執行的操做開銷不大(想一想大 O 符號),那麼你就不須要記住返回值。使用 useMemo 的成本可能會超太重新評估該函數的成本。
  • 其次,給定相同的輸入值時,對記憶(memoized)值的引用是否會發生變化?例如在上面的代碼塊中,若是 page 爲 2,type 爲「GET」,那麼對 resolvedValue 的引用是否會變化?簡單的回答是考慮 resolvedValue 變量的數據類型。若是 resolvedValue 是原始值(如字符串、數字、布爾值、空值、未定義或符號),則引用就不會變化。也就是說 ExpensiveComponent 不會被從新渲染。

修正過的代碼以下:

/**
 @param {number} page
 @param {string} type
**/
const MyComponent({page, type}) {
 const resolvedValue = getResolvedValue(page, type)
 return <ExpensiveComponent resolvedValue={resolvedValue}/>
}

如前所述,若是 resolvedValue 返回一個字符串之類的原始值,而且 getResolvedValue 這個操做的開銷沒那麼大,那麼這段代碼就很是合理,效率夠高了。

只要 page 和 type 是同樣的,好比說沒有 prop 更改,resolvedValue 的引用就會保持不變,只是返回的值不是原始值了(例如變成了對象或數組)。

記住這兩個問題:要記住的函數開銷很大嗎,返回的值是原始值嗎?每次都思考這兩個問題的話,你就能隨時判斷使用 useMemo 是否合適。

出於多種緣由須要記住默認狀態

參考如下代碼塊:

/**
 @param {number} page
 @param {string} type
**/
const myComponent({page, type}) {
 const defaultState = useMemo(() => ({
   fetched: someOperationValue(),
   type: type
 }), [type])

 const [state, setState] = useState(defaultState);
 return <ExpensiveComponent />
}

有人會以爲上面的代碼沒什麼問題,但這裏 useMemo 調用確定是沒什麼意義的。

首先咱們來試着理解一下這段代碼背後的思想。做者的思路很不錯。當 type prop 更改時他們須要新的 defaultState 對象,而且不但願在每次從新渲染時都引用 defaultState 對象。

雖然說這些問題都很實際,但這種方法是錯誤的,違反了一個基本原則:useState 是不會在每次從新渲染時都從新初始化的,只有在組件重載時纔會初始化。

傳遞給 useState 的參數更名爲 INITIAL_STATE 更合理。它只在組件剛加載時計算(或觸發)一次。

useState(INITIAL_STATE)

雖然做者擔憂在 useMemo 的 type 數組依賴項發生更改時獲取新的 defaultState 值,但這是錯誤的判斷,由於 useState 忽略了新計算的 defaultState 對象。

懶惰初始化 useState 時也是同樣的道理,以下所示:

/**
  @param {number} page
  @param {string} type
**/
const myComponent({page, type}) {
 // default state initializer
 const defaultState = () => {
   console.log("default state computed")
   return {
      fetched: someOperationValue(),
      type: type
   }
 }

 const [state, setState] = useState(defaultState);
 return <ExpensiveComponent />
}

在上面的示例中,defaultState 初始函數只會在加載時調用一次。這個函數不會在每次從新渲染時再被調用。所以「默認狀態計算」這條日誌只會出現一次,除非組件又重載了。

上面的代碼改爲這樣:

/**
  @param {number} page
  @param {string} type
**/
const myComponent({page, type}) {
 const defaultState = () => ({
    fetched: someOperationValue(),
    type,
  })

 const [state, setState] = useState(defaultState);

 // if you really need to update state based on prop change,
 // do so here
 // pseudo code - if(previousProp !== prop){setState(newStateValue)}

 return <ExpensiveComponent />
}

下面來談一些更隱蔽的場景。

把useMemo看成ESLint Hook警告
的救命稻草
image.png

看看這些評論(詳情見下方連接)就能知道,人們在千方百計避免官方的 ESLint Hooks 插件發出 lint 警告。我也很理解他們的困境。

評論連接:  https://github.com/facebook/create-react-app/issues/6880

我贊成 Dan Abramov 的觀點(詳情見下方連接)。遏制插件中的 eslint-warnings 可能會在未來某天付出相應的代價。

Dan Abramov 的觀點:
https://github.com/facebook/create-react-app/issues/6880#issuecomment-485912528

通常來講,我認爲咱們不該該在生產環境的應用程序中遏制這些警告,這樣作的話未來就更有可能出現一些隱蔽的錯誤。

話雖如此,有些狀況下咱們仍是想要遏制這些 lint 警告。如下是我遇到的一個例子。這裏的代碼是簡化過的,方便理解:

function Example ({ impressionTracker, propA, propB, propC }) {
 useEffect(() => {
   // 追蹤初始展現
   impressionTracker(propA, propB, propC)
 }, [])

 return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
}

這是一個至關棘手的問題。

在上面這個場景中你不關心 props 是否改變。你只想用隨便哪一個初始 props 調用 track 函數。這就是展現跟蹤(impression tracking)的工做機制。你只能在組件加載時調用展現跟蹤函數。這裏的區別是你須要使用一些初始 props 調用該函數。

你可能會想只要簡單地將 props 重命名爲 initialProps 之類的東西就能解決問題了,但這是行不通的。這是由於 BeautifulComponent 也須要接收更新的 prop 值。image.png

在這個示例中,你將收到 lint 警告消息:「React Hook useEffect 缺乏依賴項:'impressionTracker'、'propA'、'propB'和'propC'。能夠包含它們或刪除依賴數組。「

這條消息語氣很讓人不爽,但 linter 也只是在作本身的工做而已。簡單的解決方案是使用 eslint-disable 註釋,但這種方法不見得是最合適的,由於未來你可能在同一個 useEffect 調用中引入錯誤。

useEffect(() => {
 impressionTracker(propA, propB, propC)
 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

個人建議是使用 useRef Hook 來保持對不須要更新的初始 prop 值的引用。

function Example({impressionTracker, propA, propB, propC}) {
 // 保持對初始值的引用
 const initialTrackingValues = useRef({
     tracker: impressionTracker,
     params: {
       propA,
       propB,
       propC,
   }
 })

 // 展現跟蹤
 useEffect(() => {
   const { tracker, params } = initialTrackingValues.current;
   tracker(params)
 }, []) // 對 tracker 或 params 沒有 ESLint 警告

 return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
}

根據個人測試,在這些狀況下 linter 只會考慮 useRef。使用 useRef 後,linter 就明白引用的值不會改變,所以你不會收到任何警告!哪怕你用 useMemo 也逃不開這些警告的

例如:

function Example({impressionTracker, propA, propB, propC}) {

 // useMemo 記住這個值,使它保持不變
 const initialTrackingValues = useMemo({
   tracker: impressionTracker,
   params: {
      propA,
      propB,
      propC,
   }
 }, []) //  這裏出現 lint 警告

 // 展現跟蹤
 useEffect(() => {
   const { tracker, params} = initialTrackingValues
   tracker(params)
 }, [tracker, params]) //  這些依賴項必須放在這裏

 return <BeautifulComponent propA={propA} propB={propB} propC={propC} />
}

上面這個方案就是錯誤的,即便我用 useMemo 記憶初始 prop 值來跟蹤初始值,最後仍是無濟於事。在 useEffect 調用中,記憶值 tracker 和 params 仍然必須做爲數組依賴項輸入。

有些人就會這樣用 useMemo,這種用法不對,應該避免。咱們應該使用 useRef Hook,如前所述。

總而言之,若是你真的想要消除 lint 警告的話,你會發現 useRef 是你的好朋友。

useMemo 只用於引用相等

不少人都喜歡使用 useMemo 來處理開銷較大的計算並保持引用相等。我贊成第一條但不一樣意第二條。useMemo Hook 不該該只用於引用相等。只有一種狀況下能夠這樣作,稍後會提到。

爲何 useMemo 只用於引用相等是不對的呢?人們不都是這麼作的嗎?

參考下面的示例:

function Bla() {
 const baz = useMemo(() => [1, 2, 3], [])
 return <Foo baz={baz} />
}

在組件 Bla 中,baz 值之因此被記憶不是由於對數組 [1,2,3] 的評估開銷很大,而是由於對 baz 變量的引用在每次從新渲染時都會改變。

雖然這看起來不是個問題,但我認爲這裏不該該使用 useMemo 這個 Hook。

首先,咱們看看數組依賴。

useMemo(() => [1, 2, 3], [])

這裏,一個空數組被傳遞給 useMemo Hook。也就是說值 [1,2,3] 僅計算一次——也就是組件加載的時候。

所以咱們得出:被記憶的值計算開銷並不大,而且在加載以後不會從新計算。

出現這種狀況時,但願你能從新考慮要不要用 useMemo Hook。你正在記憶一個不是計算開銷並不大的值,它未來也不會從新計算。這不符合「memoization」一詞的定義。

這個 useMemo Hook 的用法大錯特錯。它在語義上就錯了,並且會消耗更多內存和計算資源。

那你該怎麼辦?

首先,做者在這裏究竟想要作什麼?他們不是要記住一個值;相反,他們但願在從新渲染時保持對值的 引用 不變。

別讓那條黏糊糊的變色龍鑽了空子。在這種狀況下請使用 useRef Hook。

例如,若是你真的討厭使用當前屬性(就像個人不少同事同樣),那麼只需解構並重命名便可,以下所示:

function Bla() {
 const { current: baz } = useRef([1, 2, 3])
 return <Foo baz={baz} />
}

問題解決了。

實際上,你可使用 useRef 來保持對開銷較大的函數評估的引用——只要該函數不須要在 props 更改時從新計算就沒問題。

在這些狀況下 useRef 纔是正確的 Hook,useMemo Hook 不合適。

使用 useRef Hook 來模仿實例變量是 Hook 的強大武庫中用的最少的武器之一。useRef Hook 能作的事情遠不止保持對 DOM 節點的引用。盡情擁抱它吧。

請記住這裏的條件,不要只爲了保持一致的引用就記憶一個值。若是你須要根據更改的 prop 或值從新計算該值,那就請隨意使用 useMemo Hook。在某些狀況下你仍然可使用 useRef——可是給定數組依賴列表時 useMemo 最方便。

   總結   

遠離獅子,也不要讓變色龍鑽了你的空子。若是你放進來變色龍,它們就會改變本身的膚色,融入你的代碼庫,影響你的代碼質量。別給它們機會。

英文原文: https://blog.logrocket.com/rethinking-hooks-memoization/?from=singlemessage&isappinstalled=0

 活動推薦

GMTC 全球大前端技術大會首次落地華南,走入大灣區深圳。

往屆咱們請到了來自 Google、Twitter、Instagram、阿里、騰訊、字節跳動、百度、京東、美團等國內外一線公司的頂級前端專家,分享了關於小程序、Flutter、Node、RN、前端框架、前端安全、前端工程化、移動 AI 等 50 多個熱門技術專題。目前深圳站正式啓動,7 折最低價售票通道已經開啓,詳細請諮詢:13269078023(同微信)。

相關文章
相關標籤/搜索