React.memo?React.useMemo對項目性能優(yōu)化使用詳解
React.memo
這篇文章會詳細(xì)介紹該何時、如何正確使用它,并且搭配 React.memo
來對我們的項目進(jìn)行一個性能優(yōu)化。
示例
我們先從一個簡單的示例入手
以下是一個常規(guī)的父子組件關(guān)系,打開瀏覽器控制臺并觀察,每次點擊父組件中的 +
號按鈕,都會導(dǎo)致子組件渲染。
const ReactNoMemoDemo = () => { const [count, setCount] = React.useState(0); return ( <div> <div>Parent Count: {count}</div> <button onClick={() => setCount(count => count + 1)}>+</button> <Child name="Son" /> </div> ); }; const Child = props => { console.log('子組件渲染了'); return <p>Child Name: {props.name}</p>; }; render(<ReactNoMemoDemo />);
子組件的 name
參數(shù)明明沒有被修改,為什么還是重新渲染?
這就是 React
的渲染機(jī)制,組件內(nèi)部的 state
或者 props
一旦發(fā)生修改,整個組件樹都會被重新渲染一次,即時子組件的參數(shù)沒有被修改,甚至無狀態(tài)組件。
如何處理這個問題?接下里就要說到 React.memo
介紹
React.memo 是 React
官方提供的一個高階組件,用于緩存我們的需要優(yōu)化的組件
如果你的組件在相同 props 的情況下渲染相同的結(jié)果,那么你可以通過將其包裝在 React.memo 中調(diào)用,以此通過記憶組件渲染結(jié)果的方式來提高組件的性能表現(xiàn)。這意味著在這種情況下,React 將跳過渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果。
讓我們來改進(jìn)一下上述的代碼,只需要使用 React.memo 組件包裹起來即可,其他用法不變
使用
function ReactMemoDemo() { const [count, setCount] = React.useState(0); return ( <div> <div>Parent Count: {count}</div> <button onClick={() => setCount(count => count + 1)}>+</button> <Child name="Son" /> </div> ); } const Child = React.memo(props => { console.log('子組件渲染了'); return <p>Child Name: {props.name}</p>; }); render(<ReactMemoDemo />);
再次觀察控制臺,應(yīng)該會發(fā)現(xiàn)再點擊父組件的按鈕,子組件已經(jīng)不會重新渲染了。
這就是 React.memo
為我們做的緩存優(yōu)化,渲染 Child
組件之前,對比 props
,發(fā)現(xiàn) name
沒有發(fā)生改變,因此返回了組件上一次的渲染的結(jié)果。
React.memo 僅檢查 props 變更。如果函數(shù)組件被 React.memo 包裹,且其實現(xiàn)中擁有 useState,useReducer 或 useContext 的 Hook,當(dāng) state 或 context 發(fā)生變化時,它仍會重新渲染。
當(dāng)然,如果我們子組件有內(nèi)部狀態(tài)并且發(fā)生了修改,依然會重新渲染(正常行為)。
FAQ
看到這里,不禁會產(chǎn)生疑問,既然如此,那我直接為每個組件都添加 React.memo
來進(jìn)行緩存就好了,再深究一下,為什么 React
不直接默認(rèn)為每個組件緩存呢?那這樣既節(jié)省了開發(fā)者的代碼,又為項目帶來了許多性能的優(yōu)化,這樣不好嗎?
使用太多的緩存,反而容易帶來 負(fù)提升。
前面有說到,組件使用緩存策略后,在被更新之前,會比較最新的 props
和上一次的 props
是否發(fā)生值修改,既然有比較,那就有計算,如果子組件的參數(shù)特別多且復(fù)雜繁重,那么這個比較的過程也會十分的消耗性能,甚至高于 虛擬 DOM
的生成,這時的緩存優(yōu)化,反而產(chǎn)生的負(fù)面影響,這個就是關(guān)鍵問題。
當(dāng)然,這種情況很少,大部分情況還是 組件樹的 虛擬 DOM
計算比緩存計算更消耗性能。但是,既然有這種極端問題發(fā)生,就應(yīng)該把選擇權(quán)交給開發(fā)者,讓我們自行決定是否需要對該組件進(jìn)行渲染,這也是 React
不默認(rèn)為組件設(shè)置緩存的原因。
也因此,在 React 社區(qū)中,開發(fā)者們也一致的認(rèn)為,不必要的情況下,不需要使用 React.memo
。
什么時候該用? 組件渲染過程特別消耗性能,以至于能感覺到到,比如:長列表、圖表等
什么時候不該用?組件參數(shù)結(jié)構(gòu)十分龐大復(fù)雜,比如未知層級的對象,或者列表(城市,用戶名)等
React.memo 二次優(yōu)化
React.memo
默認(rèn)每次會對復(fù)雜的對象做對比,如果你使用了 React.memo
緩存的組件參數(shù)十分復(fù)雜,且只有參數(shù)屬性內(nèi)的某些/某個字段會修改,或者根本不可能發(fā)生變化的情況下,你可以再粒度化的控制對比邏輯,通過 React.memo
第二個參數(shù)
function MyComponent(props) { /* 使用 props 渲染 */ } function shouldMemo(prevProps, nextProps) { /* 如果把 nextProps 傳入 render 方法的返回結(jié)果與 將 prevProps 傳入 render 方法的返回結(jié)果一致則返回 true, 否則返回 false */ } export default React.memo(MyComponent, shouldMemo);
如果對 class 組件有了解過的朋友應(yīng)該知道,class 組件有一個生命周期叫做 shouldComponentUpdate()
,也是通過對比 props
來告訴組件是否需要更新,但是與這個邏輯剛好相反。
小結(jié)
對于 React.memo
,無需刻意去使用它進(jìn)行緩存組件,除非你能感覺到你需要。另外,不緩存的組件會多次的觸發(fā) render
,因此,如果你在組件內(nèi)有打印信息,可能會被多次的觸發(fā),也不用去擔(dān)心,即使強(qiáng)制被 rerender
,因為狀態(tài)沒有發(fā)生改變,因此每次 render
返回的值還是一樣,所以也不會觸發(fā)真實 dom
的更新,對頁面實際沒有任何影響。
useMemo
示例
同樣,我們先看一個例子,calculatedCount
變量是一個假造的比較消耗性能的計算表達(dá)式,為了方便顯示性能數(shù)據(jù)打印時間,我們使用了 IIFE
立即執(zhí)行函數(shù),每次計算 calculatedCount
都會輸出它的計算消耗時間。
打開控制臺,因為是 IIFE
,所以首次會直接打印出時間。然后,再點擊 +
號,會發(fā)現(xiàn)再次打印出了計算耗時。這是因為 React
組件重渲染的時候,不僅是 jsx
,而且變量,函數(shù)這種也全部都會再次聲明一次,因此導(dǎo)致了 calculatedCount
重新執(zhí)行了初始化(計算),但是這個變量值并沒有發(fā)生改變,如果每次渲染都要重新計算,那也是十分的消耗性能。
注意觀察,在計算期間,頁面會發(fā)生卡死,不能操作,這是 JS 引擎 的機(jī)制,在執(zhí)行任務(wù)的時候,頁面永遠(yuǎn)不會進(jìn)行渲染,直到任務(wù)結(jié)束為止。這個過程對用戶體驗來說是致命的,雖然我們可以通過微任務(wù)去處理這個計算過程,從而避免頁面的渲染阻塞,但是消耗性能這個問題仍然存在,我們需要通過其他方式去解決。
function UseMemoDemo() { const [count, setCount] = React.useState(0); const calculatedCount = (() => { let res = 0; const startTime = Date.now(); for (let i = 0; i <= 100000000; i++) { res++; } console.log(`Calculated Count 計算耗時:${Date.now() - startTime} ms`); return res; })(); return ( <div> <div>Parent Count: {count}</div> <button onClick={() => setCount(count => count + 1)}>+</button> <div>Calculated Count: {calculatedCount}</div> </div> ); }
介紹
const memoizedValue = useMemo(() => { // 處理復(fù)雜計算,并 return 結(jié)果 }, []);
useMemo
返回一個緩存過的值,把 "創(chuàng)建" 函數(shù)和依賴項數(shù)組作為參數(shù)傳入 useMemo
,它僅會在某個依賴項改變時才重新計算 memoized
值。這種優(yōu)化有助于避免在每次渲染時都進(jìn)行高開銷的計算
第一個參數(shù)是函數(shù),函數(shù)中需要返回計算值
第二個參數(shù)是依賴數(shù)組
- 如果不傳,則每次都會初始化,緩存失敗
- 如果傳空數(shù)組,則永遠(yuǎn)都會返回第一次執(zhí)行的結(jié)果
- 如果傳狀態(tài),則在依賴的狀態(tài)變化時,才會從新計算,如果這個緩存狀態(tài)依賴了其他狀態(tài)的話,則需要提供進(jìn)去。
這下就很好理解了,我們的 calculatedCount
沒有任何外部依賴,因此只需要傳遞空數(shù)組作為第二個參數(shù),開始改造
使用
function UseMemoDemo() { const [count, setCount] = React.useState(0); const calculatedCount = useMemo(() => { let res = 0; const startTime = Date.now(); for (let i = 0; i <= 100000000; i++) { res++; } console.log(`Memo Calculated Count 計算耗時:${Date.now() - startTime} ms`); return res; }, []); return ( <div> <div>Parent Count: {count}</div> <button onClick={() => setCount(count => count + 1)}>+</button> <div>Memorized Calculated Count: {calculatedCount}</div> </div> ); }
現(xiàn)在,"Memo Calculated Count 計算耗時"的輸出信息永遠(yuǎn)只會打印一次,因為它被無限緩存了。
FAQ何時使用?
當(dāng)你的表達(dá)式十分復(fù)雜需要經(jīng)過大量計算的時候
示例
下面示例中,我們使用狀態(tài)提升,將子組件的 click
事件函數(shù)放在了父組件中,點擊父組件的 +
號,發(fā)現(xiàn)子組件被重新渲染
const FunctionPropDemo = () => { const [count, setCount] = React.useState(0); const handleChildClick = () => { // }; return ( <div> <div>Parent Count: {count}</div> <button onClick={() => setCount(count => count + 1)}>+</button> <Child onClick={handleChildClick} /> </div> ); }; const Child = React.memo(props => { console.log('子組件渲染了'); return ( <div> <div>Child</div> <button onClick={props.onClick}>Click Me</button> </div> ); }); render(<FunctionPropDemo />);
于是我們想到用 memo
函數(shù)包裹子組件,給緩存起來
const FunctionPropDemo = () => { const [count, setCount] = React.useState(0); const handleChildClick = () => { // }; return ( <div> <div>Parent Count: {count}</div> <button onClick={() => setCount(count => count + 1)}>+</button> <Child onClick={handleChildClick} /> </div> ); }; const Child = React.memo(props => { console.log('子組件渲染了'); return ( <div> <div>Child</div> <button onClick={props.onClick}>Click Me</button> </div> ); }); render(<FunctionPropDemo />);
但是意外來了,即使被 memo
包裹的組件,還是被重新渲染了,為什么!
我們來逐一分析
- 首先,點擊父組件的
+
號,count
發(fā)生變化,于是父組件開始重渲染 - 內(nèi)部的未經(jīng)處理的變量和函數(shù)都被重新初始化,
useState
不會再初始化了, useEffect 鉤子函數(shù)重新執(zhí)行,虛擬 dom 更新 - 執(zhí)行到
Child
組件的時候,Child
準(zhǔn)備更新,但是因為它是memo
緩存組件,于是開始淺比較props
參數(shù),到這里為止一切正常 Child
組件參數(shù)開始逐一比較變更,到了onClick
函數(shù),發(fā)現(xiàn)值為函數(shù),提供的新值也為函數(shù),但是因為剛剛在父組件內(nèi)部重渲染時被重新初始化了(生成了新的地址),因為函數(shù)是引用類型值,導(dǎo)致引用地址發(fā)生改變!比較結(jié)果為不相等,React
仍會認(rèn)為它已更改,因此重新發(fā)生了渲染。
既然函數(shù)重新渲染會被重新初始化生成新的引用地址,因此我們應(yīng)該避免它重新初始化。這個時候,useMemo
的第二個使用場景就來了
const FunctionPropDemo = () => { const [count, setCount] = React.useState(0); const handleChildClick = useMemo(() => { return () => { // }; }, []); return ( <div> <div>Parent Count: {count}</div> <button onClick={() => setCount(count => count + 1)}>+</button> <Child onClick={handleChildClick} /> </div> ); }; const Child = React.memo(props => { console.log('子組件渲染了'); return ( <div> <div>Child</div> <button onClick={props.onClick}>Click Me</button> </div> ); }); render(<FunctionPropDemo />);
這里我們將原本的 handleChildClick
函數(shù)通過 useMemo
包裹起來了,另外函數(shù)永遠(yuǎn)不會發(fā)生改變,因此傳遞第二參數(shù)為空數(shù)組,再次嘗試點擊 +
號,子組件不會被重新渲染了。
對于對象,數(shù)組,renderProps
(參數(shù)為 react
組件) 等參數(shù),都可以使用 useMemo
進(jìn)行緩存
示例
既然 useMemo
可以緩存變量函數(shù)等,那組件其實也是一個函數(shù),能不能被緩存呢?我們試一試
繼續(xù)使用第一個案例,將 React.memo 移除,使用 useMemo
改造
const ReactNoMemoDemo = () => { const [count, setCount] = React.useState(0); const memorizedChild = useMemo(() => <Child name="Son" />, []); return ( <div> <div>Parent Count: {count}</div> <button onClick={() => setCount(count => count + 1)}>+</button> {memorizedChild} </div> ); }; const Child = props => { console.log('子組件渲染了'); return <p>Child Name: {props.name}</p>; }; render(<ReactNoMemoDemo />);
嘗試點擊 +
號,是的,Child
被 useMemo
緩存成功了!
小結(jié)
同樣的,不是必要的情況下,和 React.memo
一樣,不需要特別的使用 useMemo
使用場景
- 表達(dá)式有復(fù)雜計算且不會頻發(fā)觸發(fā)更新
- 引用類型的組件參數(shù),函數(shù),對象,數(shù)組等(一般情況下對象和數(shù)組都會從
useState
初始化,useState
不會二次執(zhí)行,主要是函數(shù)參數(shù)) react
組件的緩存
擴(kuò)展
useCallback
前面使用 useMemo 包裹了函數(shù),會感覺代碼結(jié)構(gòu)非常的奇怪
const handleChildClick = useMemo(() => { return () => { // }; }, []);
函數(shù)中又 return
了一個函數(shù),其實還有另一個推薦的 API
, useCallback
來代替于對函數(shù)的緩存,兩者功能是完全一樣,只是使用方法的區(qū)別,useMemo
需要從第一個函數(shù)參數(shù)中 return
出要緩存的函數(shù),useCallback
則直接將函數(shù)傳入第一個參數(shù)即可
const handleChildClick = useCallback(() => { // }, []);
代碼風(fēng)格上簡介明了了許多
看完這篇文章,相信你對 React.memo
和 React.useMemo
已經(jīng)有了一定的了解,并且知道何時/如何使用它們了
以上就是React.memo React.useMemo對項目性能優(yōu)化使用詳解的詳細(xì)內(nèi)容,更多關(guān)于React.memo React.useMemo性能優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解React Angular Vue三大前端技術(shù)
當(dāng)前世界中,技術(shù)發(fā)展非常迅速并且變化迅速,開發(fā)者需要更多的開發(fā)工具來解決不同的問題。本文就對于當(dāng)下主流的前端開發(fā)技術(shù)React、Vue、Angular這三個框架做個相對詳盡的探究,目的是為了解開這些前端技術(shù)的面紗,看看各自的廬山真面目。2021-05-05React Native中導(dǎo)航組件react-navigation跨tab路由處理詳解
這篇文章主要給大家介紹了關(guān)于React Native中導(dǎo)航組件react-navigation跨tab路由處理的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10