網站首頁 編程語言 正文
React.memo
這篇文章會詳細介紹該何時、如何正確使用它,并且搭配 React.memo
來對我們的項目進行一個性能優化。
示例
我們先從一個簡單的示例入手
以下是一個常規的父子組件關系,打開瀏覽器控制臺并觀察,每次點擊父組件中的 +
號按鈕,都會導致子組件渲染。
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
參數明明沒有被修改,為什么還是重新渲染?
這就是 React
的渲染機制,組件內部的 state
或者 props
一旦發生修改,整個組件樹都會被重新渲染一次,即時子組件的參數沒有被修改,甚至無狀態組件。
如何處理這個問題?接下里就要說到 React.memo
介紹
React.memo 是 React
官方提供的一個高階組件,用于緩存我們的需要優化的組件
如果你的組件在相同 props 的情況下渲染相同的結果,那么你可以通過將其包裝在 React.memo 中調用,以此通過記憶組件渲染結果的方式來提高組件的性能表現。這意味著在這種情況下,React 將跳過渲染組件的操作并直接復用最近一次渲染的結果。
讓我們來改進一下上述的代碼,只需要使用 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 />);
再次觀察控制臺,應該會發現再點擊父組件的按鈕,子組件已經不會重新渲染了。
這就是 React.memo
為我們做的緩存優化,渲染 Child
組件之前,對比 props
,發現 name
沒有發生改變,因此返回了組件上一次的渲染的結果。
React.memo 僅檢查 props 變更。如果函數組件被 React.memo 包裹,且其實現中擁有 useState,useReducer 或 useContext 的 Hook,當 state 或 context 發生變化時,它仍會重新渲染。
當然,如果我們子組件有內部狀態并且發生了修改,依然會重新渲染(正常行為)。
FAQ
看到這里,不禁會產生疑問,既然如此,那我直接為每個組件都添加 React.memo
來進行緩存就好了,再深究一下,為什么 React
不直接默認為每個組件緩存呢?那這樣既節省了開發者的代碼,又為項目帶來了許多性能的優化,這樣不好嗎?
使用太多的緩存,反而容易帶來 負提升。
前面有說到,組件使用緩存策略后,在被更新之前,會比較最新的 props
和上一次的 props
是否發生值修改,既然有比較,那就有計算,如果子組件的參數特別多且復雜繁重,那么這個比較的過程也會十分的消耗性能,甚至高于 虛擬 DOM
的生成,這時的緩存優化,反而產生的負面影響,這個就是關鍵問題。
當然,這種情況很少,大部分情況還是 組件樹的 虛擬 DOM
計算比緩存計算更消耗性能。但是,既然有這種極端問題發生,就應該把選擇權交給開發者,讓我們自行決定是否需要對該組件進行渲染,這也是 React
不默認為組件設置緩存的原因。
也因此,在 React 社區中,開發者們也一致的認為,不必要的情況下,不需要使用 React.memo
。
什么時候該用? 組件渲染過程特別消耗性能,以至于能感覺到到,比如:長列表、圖表等
什么時候不該用?組件參數結構十分龐大復雜,比如未知層級的對象,或者列表(城市,用戶名)等
React.memo 二次優化
React.memo
默認每次會對復雜的對象做對比,如果你使用了 React.memo
緩存的組件參數十分復雜,且只有參數屬性內的某些/某個字段會修改,或者根本不可能發生變化的情況下,你可以再粒度化的控制對比邏輯,通過 React.memo
第二個參數
function MyComponent(props) { /* 使用 props 渲染 */ } function shouldMemo(prevProps, nextProps) { /* 如果把 nextProps 傳入 render 方法的返回結果與 將 prevProps 傳入 render 方法的返回結果一致則返回 true, 否則返回 false */ } export default React.memo(MyComponent, shouldMemo);
如果對 class 組件有了解過的朋友應該知道,class 組件有一個生命周期叫做 shouldComponentUpdate()
,也是通過對比 props
來告訴組件是否需要更新,但是與這個邏輯剛好相反。
小結
對于 React.memo
,無需刻意去使用它進行緩存組件,除非你能感覺到你需要。另外,不緩存的組件會多次的觸發 render
,因此,如果你在組件內有打印信息,可能會被多次的觸發,也不用去擔心,即使強制被 rerender
,因為狀態沒有發生改變,因此每次 render
返回的值還是一樣,所以也不會觸發真實 dom
的更新,對頁面實際沒有任何影響。
useMemo
示例
同樣,我們先看一個例子,calculatedCount
變量是一個假造的比較消耗性能的計算表達式,為了方便顯示性能數據打印時間,我們使用了 IIFE
立即執行函數,每次計算 calculatedCount
都會輸出它的計算消耗時間。
打開控制臺,因為是 IIFE
,所以首次會直接打印出時間。然后,再點擊 +
號,會發現再次打印出了計算耗時。這是因為 React
組件重渲染的時候,不僅是 jsx
,而且變量,函數這種也全部都會再次聲明一次,因此導致了 calculatedCount
重新執行了初始化(計算),但是這個變量值并沒有發生改變,如果每次渲染都要重新計算,那也是十分的消耗性能。
注意觀察,在計算期間,頁面會發生卡死,不能操作,這是 JS 引擎 的機制,在執行任務的時候,頁面永遠不會進行渲染,直到任務結束為止。這個過程對用戶體驗來說是致命的,雖然我們可以通過微任務去處理這個計算過程,從而避免頁面的渲染阻塞,但是消耗性能這個問題仍然存在,我們需要通過其他方式去解決。
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 結果 }, []);
useMemo
返回一個緩存過的值,把 "創建" 函數和依賴項數組作為參數傳入 useMemo
,它僅會在某個依賴項改變時才重新計算 memoized
值。這種優化有助于避免在每次渲染時都進行高開銷的計算
第一個參數是函數,函數中需要返回計算值
第二個參數是依賴數組
- 如果不傳,則每次都會初始化,緩存失敗
- 如果傳空數組,則永遠都會返回第一次執行的結果
- 如果傳狀態,則在依賴的狀態變化時,才會從新計算,如果這個緩存狀態依賴了其他狀態的話,則需要提供進去。
這下就很好理解了,我們的 calculatedCount
沒有任何外部依賴,因此只需要傳遞空數組作為第二個參數,開始改造
使用
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> ); }
現在,"Memo Calculated Count 計算耗時"的輸出信息永遠只會打印一次,因為它被無限緩存了。
FAQ何時使用?
當你的表達式十分復雜需要經過大量計算的時候
示例
下面示例中,我們使用狀態提升,將子組件的 click
事件函數放在了父組件中,點擊父組件的 +
號,發現子組件被重新渲染
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
函數包裹子組件,給緩存起來
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
發生變化,于是父組件開始重渲染 - 內部的未經處理的變量和函數都被重新初始化,
useState
不會再初始化了, useEffect 鉤子函數重新執行,虛擬 dom 更新 - 執行到
Child
組件的時候,Child
準備更新,但是因為它是memo
緩存組件,于是開始淺比較props
參數,到這里為止一切正常 -
Child
組件參數開始逐一比較變更,到了onClick
函數,發現值為函數,提供的新值也為函數,但是因為剛剛在父組件內部重渲染時被重新初始化了(生成了新的地址),因為函數是引用類型值,導致引用地址發生改變!比較結果為不相等,React
仍會認為它已更改,因此重新發生了渲染。
既然函數重新渲染會被重新初始化生成新的引用地址,因此我們應該避免它重新初始化。這個時候,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
函數通過 useMemo
包裹起來了,另外函數永遠不會發生改變,因此傳遞第二參數為空數組,再次嘗試點擊 +
號,子組件不會被重新渲染了。
對于對象,數組,renderProps
(參數為 react
組件) 等參數,都可以使用 useMemo
進行緩存
示例
既然 useMemo
可以緩存變量函數等,那組件其實也是一個函數,能不能被緩存呢?我們試一試
繼續使用第一個案例,將 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
緩存成功了!
小結
同樣的,不是必要的情況下,和 React.memo
一樣,不需要特別的使用 useMemo
使用場景
- 表達式有復雜計算且不會頻發觸發更新
- 引用類型的組件參數,函數,對象,數組等(一般情況下對象和數組都會從
useState
初始化,useState
不會二次執行,主要是函數參數) -
react
組件的緩存
擴展
useCallback
前面使用 useMemo 包裹了函數,會感覺代碼結構非常的奇怪
const handleChildClick = useMemo(() => { return () => { // }; }, []);
函數中又 return
了一個函數,其實還有另一個推薦的 API
, useCallback
來代替于對函數的緩存,兩者功能是完全一樣,只是使用方法的區別,useMemo
需要從第一個函數參數中 return
出要緩存的函數,useCallback
則直接將函數傳入第一個參數即可
const handleChildClick = useCallback(() => { // }, []);
代碼風格上簡介明了了許多
看完這篇文章,相信你對 React.memo
和 React.useMemo
已經有了一定的了解,并且知道何時/如何使用它們了
原文鏈接:https://juejin.cn/post/7188041140963115066
相關推薦
- 2024-07-15 SpringBoot使用Apache Poi導出word文檔
- 2022-05-09 Python的ini配置文件你了解嗎_python
- 2024-03-19 maven本地倉庫有包,導致could not find artifact
- 2022-03-31 用C語言實現排雷游戲_C 語言
- 2022-12-27 python空值判斷方式(if?xxx和if?xxx?is?None的區別及說明)_python
- 2022-05-23 python中的netCDF4批量處理NC文件的操作方法_python
- 2024-02-27 var、let和const區別
- 2022-11-17 Android開發Flutter?桌面應用窗口化實戰示例_Android
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支