奇怪的useMemo知識(shí)增加了
作為「性能優(yōu)化」手段,一般用useMemo緩存函數(shù)組件中比較消耗性能的計(jì)算結(jié)果:
function?App()?{
??const?memoizedValue?=?useMemo(
????()?=>?computeExpensiveValue(a,?b),
????[a,?b]
??);
??//?...
}
只有在依賴項(xiàng)改變后才會(huì)重新計(jì)算新的memoizedValue。
你有沒有想過,如果用useMemo緩存函數(shù)組件的返回值,會(huì)怎么樣呢?
舉個(gè)例子
我們有個(gè)全局context —— AppContext。
由于同學(xué)們偷懶,隨著項(xiàng)目的迭代,新增的context都選擇放在AppContext里,導(dǎo)致AppContext包含的內(nèi)容越來越多。
現(xiàn)在我們有個(gè)Tree組件,他會(huì)渲染一個(gè)很耗性能的大組件ExpensiveTree。
function?Tree()?{
??let?appContextValue?=?useContext(AppContext);
??let?theme?=?appContextValue.theme;
??return?<ExpensiveTree?className={theme}?/>;
}
該組件內(nèi)部依賴AppContext中的theme狀態(tài)。
由于AppContext中包含很多與theme無關(guān)的state,導(dǎo)致每次其他無關(guān)的state更新,Tree都會(huì)重新render,進(jìn)而ExpensiveTree組件也重新render。
現(xiàn)在這個(gè)優(yōu)化任務(wù)交到了你手上,該怎么辦呢?
優(yōu)化ExpensiveTree
這時(shí)候,useMemo就能派上用場(chǎng):
function?Tree()?{
??let?appContextValue?=?useContext(AppContext);
??let?theme?=?appContextValue.theme;
??return?useMemo(()?=>?{
????return?<ExpensiveTree?className={theme}?/>;
??},?[theme])
}
我們將返回的ExpensiveTree作為useMemo返回值,theme作為依賴。
這樣,即使AppContext改變導(dǎo)致Tree反復(fù)render,ExpensiveTree也只會(huì)在theme改變后render。

原理解析
要理解這么做有效的原因,需要了解三點(diǎn):
useMemo返回值是什么函數(shù)組件的返回值是什么
React組件在什么時(shí)候render
回答第一個(gè)問題:useMemo會(huì)將第一個(gè)參數(shù)(函數(shù))的返回值保存在組件對(duì)應(yīng)fiber中,只有在依賴項(xiàng)(第二個(gè)參數(shù))變化后才會(huì)重新調(diào)用第一個(gè)參數(shù)(函數(shù))計(jì)算一個(gè)新值。
回答第二個(gè)問題:函數(shù)組件的返回值是JSX對(duì)象。
同一個(gè)函數(shù)組件調(diào)用多次,返回的是多個(gè)「不同」的JSX對(duì)象(即使props未變,但JSX是新的引用)。
按照以上兩個(gè)回答,我們可以得出結(jié)論:
以上
useMemo用法實(shí)際上在函數(shù)組件對(duì)應(yīng)的fiber中緩存了一個(gè)完整的JSX對(duì)象
第三個(gè)問題,函數(shù)組件需要同時(shí)滿足如下條件才不會(huì)render:
oldProps === newProps
前后兩次更新props全等,注意是「全等」。
組件
context沒有變化workInProgress.type === current.type
組件更新前后fiber.type未變化,比如div沒有變?yōu)?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(145, 109, 213);font-weight: bolder;background-image: none;background-position: initial;background-size: initial;background-repeat: initial;background-attachment: initial;background-origin: initial;background-clip: initial;">p。
!includesSomeLane(renderLanes, updateLanes)
當(dāng)前fiber上不存在更新,或者存在更新但優(yōu)先級(jí)低。
更詳細(xì)的解釋,可以參考這篇文章:React組件到底什么時(shí)候render?
當(dāng)我們不使用useMemo包裹返回值,每次Tree render返回的都是全新的JSX對(duì)象。
所以對(duì)于ExpensiveTree,oldProps !== newProps。
再看2:ExpensiveTree內(nèi)部context沒變,滿足
再看3:ExpensiveTree更新前后type都是ExpensiveTree,滿足
再看4: ExpensiveTree內(nèi)沒有狀態(tài)更新,滿足
所以,當(dāng)我們使用useMemo包裹ExpensiveTree后,當(dāng)theme不變,每次Tree render后返回的都是同一個(gè)JSX對(duì)象,滿足第一條。
基于這個(gè)原因,ExpensiveTree不會(huì)render。
總結(jié)
這篇文章提到的useMemo用法,并未在官網(wǎng)文檔中體現(xiàn),而是在#15156[1]中由Dan介紹。
相比Vue,React更靈活,開發(fā)過程中需要開發(fā)者注意更多細(xì)節(jié)。要完全了解React,可能需要學(xué)習(xí)一些源碼層面的知識(shí)。
參考資料
#15156: https://github.com/facebook/react/issues/15156#issuecomment-474590693
