React.memo?React.useMemo對項目性能優(yōu)化使用詳解
React.memo
這篇文章會詳細介紹該何時、如何正確使用它,并且搭配 React.memo
來對我們的項目進行一個性能優(yōu)化。
示例
我們先從一個簡單的示例入手
以下是一個常規(guī)的父子組件關系,打開瀏覽器控制臺并觀察,每次點擊父組件中的 +
號按鈕,都會導致子組件渲染。
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
的渲染機制,組件內(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 將跳過渲染組件的操作并直接復用最近一次渲染的結(jié)果。
讓我們來改進一下上述的代碼,只需要使用 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 />);
再次觀察控制臺,應該會發(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,當 state 或 context 發(fā)生變化時,它仍會重新渲染。
當然,如果我們子組件有內(nèi)部狀態(tài)并且發(fā)生了修改,依然會重新渲染(正常行為)。
FAQ
看到這里,不禁會產(chǎn)生疑問,既然如此,那我直接為每個組件都添加 React.memo
來進行緩存就好了,再深究一下,為什么 React
不直接默認為每個組件緩存呢?那這樣既節(jié)省了開發(fā)者的代碼,又為項目帶來了許多性能的優(yōu)化,這樣不好嗎?
使用太多的緩存,反而容易帶來 負提升。
前面有說到,組件使用緩存策略后,在被更新之前,會比較最新的 props
和上一次的 props
是否發(fā)生值修改,既然有比較,那就有計算,如果子組件的參數(shù)特別多且復雜繁重,那么這個比較的過程也會十分的消耗性能,甚至高于 虛擬 DOM
的生成,這時的緩存優(yōu)化,反而產(chǎn)生的負面影響,這個就是關鍵問題。
當然,這種情況很少,大部分情況還是 組件樹的 虛擬 DOM
計算比緩存計算更消耗性能。但是,既然有這種極端問題發(fā)生,就應該把選擇權(quán)交給開發(fā)者,讓我們自行決定是否需要對該組件進行渲染,這也是 React
不默認為組件設置緩存的原因。
也因此,在 React 社區(qū)中,開發(fā)者們也一致的認為,不必要的情況下,不需要使用 React.memo
。
什么時候該用? 組件渲染過程特別消耗性能,以至于能感覺到到,比如:長列表、圖表等
什么時候不該用?組件參數(shù)結(jié)構(gòu)十分龐大復雜,比如未知層級的對象,或者列表(城市,用戶名)等
React.memo 二次優(yōu)化
React.memo
默認每次會對復雜的對象做對比,如果你使用了 React.memo
緩存的組件參數(shù)十分復雜,且只有參數(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 組件有了解過的朋友應該知道,class 組件有一個生命周期叫做 shouldComponentUpdate()
,也是通過對比 props
來告訴組件是否需要更新,但是與這個邏輯剛好相反。
小結(jié)
對于 React.memo
,無需刻意去使用它進行緩存組件,除非你能感覺到你需要。另外,不緩存的組件會多次的觸發(fā) render
,因此,如果你在組件內(nèi)有打印信息,可能會被多次的觸發(fā),也不用去擔心,即使強制被 rerender
,因為狀態(tài)沒有發(fā)生改變,因此每次 render
返回的值還是一樣,所以也不會觸發(fā)真實 dom
的更新,對頁面實際沒有任何影響。
useMemo
示例
同樣,我們先看一個例子,calculatedCount
變量是一個假造的比較消耗性能的計算表達式,為了方便顯示性能數(shù)據(jù)打印時間,我們使用了 IIFE
立即執(zhí)行函數(shù),每次計算 calculatedCount
都會輸出它的計算消耗時間。
打開控制臺,因為是 IIFE
,所以首次會直接打印出時間。然后,再點擊 +
號,會發(fā)現(xiàn)再次打印出了計算耗時。這是因為 React
組件重渲染的時候,不僅是 jsx
,而且變量,函數(shù)這種也全部都會再次聲明一次,因此導致了 calculatedCount
重新執(zhí)行了初始化(計算),但是這個變量值并沒有發(fā)生改變,如果每次渲染都要重新計算,那也是十分的消耗性能。
注意觀察,在計算期間,頁面會發(fā)生卡死,不能操作,這是 JS 引擎 的機制,在執(zhí)行任務的時候,頁面永遠不會進行渲染,直到任務結(jié)束為止。這個過程對用戶體驗來說是致命的,雖然我們可以通過微任務去處理這個計算過程,從而避免頁面的渲染阻塞,但是消耗性能這個問題仍然存在,我們需要通過其他方式去解決。
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(() => { // 處理復雜計算,并 return 結(jié)果 }, []);
useMemo
返回一個緩存過的值,把 "創(chuàng)建" 函數(shù)和依賴項數(shù)組作為參數(shù)傳入 useMemo
,它僅會在某個依賴項改變時才重新計算 memoized
值。這種優(yōu)化有助于避免在每次渲染時都進行高開銷的計算
第一個參數(shù)是函數(shù),函數(shù)中需要返回計算值
第二個參數(shù)是依賴數(shù)組
- 如果不傳,則每次都會初始化,緩存失敗
- 如果傳空數(shù)組,則永遠都會返回第一次執(zhí)行的結(jié)果
- 如果傳狀態(tài),則在依賴的狀態(tài)變化時,才會從新計算,如果這個緩存狀態(tài)依賴了其他狀態(tài)的話,則需要提供進去。
這下就很好理解了,我們的 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 計算耗時"的輸出信息永遠只會打印一次,因為它被無限緩存了。
FAQ何時使用?
當你的表達式十分復雜需要經(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
準備更新,但是因為它是memo
緩存組件,于是開始淺比較props
參數(shù),到這里為止一切正常 Child
組件參數(shù)開始逐一比較變更,到了onClick
函數(shù),發(fā)現(xiàn)值為函數(shù),提供的新值也為函數(shù),但是因為剛剛在父組件內(nèi)部重渲染時被重新初始化了(生成了新的地址),因為函數(shù)是引用類型值,導致引用地址發(fā)生改變!比較結(jié)果為不相等,React
仍會認為它已更改,因此重新發(fā)生了渲染。
既然函數(shù)重新渲染會被重新初始化生成新的引用地址,因此我們應該避免它重新初始化。這個時候,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ù)永遠不會發(fā)生改變,因此傳遞第二參數(shù)為空數(shù)組,再次嘗試點擊 +
號,子組件不會被重新渲染了。
對于對象,數(shù)組,renderProps
(參數(shù)為 react
組件) 等參數(shù),都可以使用 useMemo
進行緩存
示例
既然 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
使用場景
- 表達式有復雜計算且不會頻發(fā)觸發(fā)更新
- 引用類型的組件參數(shù),函數(shù),對象,數(shù)組等(一般情況下對象和數(shù)組都會從
useState
初始化,useState
不會二次執(zhí)行,主要是函數(shù)參數(shù)) react
組件的緩存
擴展
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(() => { // }, []);
代碼風格上簡介明了了許多
看完這篇文章,相信你對 React.memo
和 React.useMemo
已經(jīng)有了一定的了解,并且知道何時/如何使用它們了
以上就是React.memo React.useMemo對項目性能優(yōu)化使用詳解的詳細內(nèi)容,更多關于React.memo React.useMemo性能優(yōu)化的資料請關注腳本之家其它相關文章!
相關文章
React Native中導航組件react-navigation跨tab路由處理詳解
這篇文章主要給大家介紹了關于React Native中導航組件react-navigation跨tab路由處理的相關資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。2017-10-10