做者 | Ohans Emmanuel譯者 | 王強編輯 | Yonie在使用 React Hooks 的過程當中,做者發現過渡頻繁的應用 useMemo 用會影響程序的性能。在本文中做者將與你們分享如何避免過分使用 useMemo,使程序遠離性能問題。前端
通過觀察總結,我發如今兩種狀況下 useMemo 要麼沒什麼用,要麼就是用得太多了,並且可能會影響應用程序的性能表現。react
第一種狀況很容易就能推斷出來,可是第二種狀況就比較隱蔽了,很容易被忽略。若是你在生產環境的應用程序中使用了 Hook,那麼你就可能會在這兩個場景中使用 useMemo Hook。git
下面我就會談一談爲何這些 useMemo 沒什麼必要,甚至可能影響你的應用性能。此外我會教你們在這些場景中避免過分使用 useMemo 的方法。github
咱們開始吧。小程序
不須要 useMemo 的狀況爲了方便,咱們把這兩類場景分別稱爲獅子和變色龍。前端工程化
先不用糾結爲何這麼叫,繼續讀下去就是。數組
當你撞上一頭雄獅,你的第一反應就是撒丫子跑,不要成爲獅子的盤中餐,而後活下來跟別人吹牛。這時候可沒空思考那麼多。安全
這就是場景 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 以前都應該考慮兩個問題:其次,給定相同的輸入值時,對記憶(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警告看看這些評論(詳情見下方連接)就能知道,人們在千方百計避免官方的 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 值。
在這個示例中,你將收到 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(同微信)。