網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
React.memo
這篇文章會(huì)詳細(xì)介紹該何時(shí)、如何正確使用它,并且搭配 React.memo
來(lái)對(duì)我們的項(xiàng)目進(jìn)行一個(gè)性能優(yōu)化。
示例
我們先從一個(gè)簡(jiǎn)單的示例入手
以下是一個(gè)常規(guī)的父子組件關(guān)系,打開(kāi)瀏覽器控制臺(tái)并觀察,每次點(diǎn)擊父組件中的 +
號(hào)按鈕,都會(huì)導(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ù)明明沒(méi)有被修改,為什么還是重新渲染?
這就是 React
的渲染機(jī)制,組件內(nèi)部的 state
或者 props
一旦發(fā)生修改,整個(gè)組件樹(shù)都會(huì)被重新渲染一次,即時(shí)子組件的參數(shù)沒(méi)有被修改,甚至無(wú)狀態(tài)組件。
如何處理這個(gè)問(wèn)題?接下里就要說(shuō)到 React.memo
介紹
React.memo 是 React
官方提供的一個(gè)高階組件,用于緩存我們的需要優(yōu)化的組件
如果你的組件在相同 props 的情況下渲染相同的結(jié)果,那么你可以通過(guò)將其包裝在 React.memo 中調(diào)用,以此通過(guò)記憶組件渲染結(jié)果的方式來(lái)提高組件的性能表現(xiàn)。這意味著在這種情況下,React 將跳過(guò)渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果。
讓我們來(lái)改進(jìn)一下上述的代碼,只需要使用 React.memo 組件包裹起來(lái)即可,其他用法不變
使用
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 />);
再次觀察控制臺(tái),應(yīng)該會(huì)發(fā)現(xiàn)再點(diǎn)擊父組件的按鈕,子組件已經(jīng)不會(huì)重新渲染了。
這就是 React.memo
為我們做的緩存優(yōu)化,渲染 Child
組件之前,對(duì)比 props
,發(fā)現(xiàn) name
沒(méi)有發(fā)生改變,因此返回了組件上一次的渲染的結(jié)果。
React.memo 僅檢查 props 變更。如果函數(shù)組件被 React.memo 包裹,且其實(shí)現(xiàn)中擁有 useState,useReducer 或 useContext 的 Hook,當(dāng) state 或 context 發(fā)生變化時(shí),它仍會(huì)重新渲染。
當(dāng)然,如果我們子組件有內(nèi)部狀態(tài)并且發(fā)生了修改,依然會(huì)重新渲染(正常行為)。
FAQ
看到這里,不禁會(huì)產(chǎn)生疑問(wèn),既然如此,那我直接為每個(gè)組件都添加 React.memo
來(lái)進(jìn)行緩存就好了,再深究一下,為什么 React
不直接默認(rèn)為每個(gè)組件緩存呢?那這樣既節(jié)省了開(kāi)發(fā)者的代碼,又為項(xiàng)目帶來(lái)了許多性能的優(yōu)化,這樣不好嗎?
使用太多的緩存,反而容易帶來(lái) 負(fù)提升。
前面有說(shuō)到,組件使用緩存策略后,在被更新之前,會(huì)比較最新的 props
和上一次的 props
是否發(fā)生值修改,既然有比較,那就有計(jì)算,如果子組件的參數(shù)特別多且復(fù)雜繁重,那么這個(gè)比較的過(guò)程也會(huì)十分的消耗性能,甚至高于 虛擬 DOM
的生成,這時(shí)的緩存優(yōu)化,反而產(chǎn)生的負(fù)面影響,這個(gè)就是關(guān)鍵問(wèn)題。
當(dāng)然,這種情況很少,大部分情況還是 組件樹(shù)的 虛擬 DOM
計(jì)算比緩存計(jì)算更消耗性能。但是,既然有這種極端問(wèn)題發(fā)生,就應(yīng)該把選擇權(quán)交給開(kāi)發(fā)者,讓我們自行決定是否需要對(duì)該組件進(jìn)行渲染,這也是 React
不默認(rèn)為組件設(shè)置緩存的原因。
也因此,在 React 社區(qū)中,開(kāi)發(fā)者們也一致的認(rèn)為,不必要的情況下,不需要使用 React.memo
。
什么時(shí)候該用? 組件渲染過(guò)程特別消耗性能,以至于能感覺(jué)到到,比如:長(zhǎng)列表、圖表等
什么時(shí)候不該用?組件參數(shù)結(jié)構(gòu)十分龐大復(fù)雜,比如未知層級(jí)的對(duì)象,或者列表(城市,用戶(hù)名)等
React.memo 二次優(yōu)化
React.memo
默認(rèn)每次會(huì)對(duì)復(fù)雜的對(duì)象做對(duì)比,如果你使用了 React.memo
緩存的組件參數(shù)十分復(fù)雜,且只有參數(shù)屬性?xún)?nèi)的某些/某個(gè)字段會(huì)修改,或者根本不可能發(fā)生變化的情況下,你可以再粒度化的控制對(duì)比邏輯,通過(guò) React.memo
第二個(gè)參數(shù)
function MyComponent(props) { /* 使用 props 渲染 */ } function shouldMemo(prevProps, nextProps) { /* 如果把 nextProps 傳入 render 方法的返回結(jié)果與 將 prevProps 傳入 render 方法的返回結(jié)果一致則返回 true, 否則返回 false */ } export default React.memo(MyComponent, shouldMemo);
如果對(duì) class 組件有了解過(guò)的朋友應(yīng)該知道,class 組件有一個(gè)生命周期叫做 shouldComponentUpdate()
,也是通過(guò)對(duì)比 props
來(lái)告訴組件是否需要更新,但是與這個(gè)邏輯剛好相反。
小結(jié)
對(duì)于 React.memo
,無(wú)需刻意去使用它進(jìn)行緩存組件,除非你能感覺(jué)到你需要。另外,不緩存的組件會(huì)多次的觸發(fā) render
,因此,如果你在組件內(nèi)有打印信息,可能會(huì)被多次的觸發(fā),也不用去擔(dān)心,即使強(qiáng)制被 rerender
,因?yàn)闋顟B(tài)沒(méi)有發(fā)生改變,因此每次 render
返回的值還是一樣,所以也不會(huì)觸發(fā)真實(shí) dom
的更新,對(duì)頁(yè)面實(shí)際沒(méi)有任何影響。
useMemo
示例
同樣,我們先看一個(gè)例子,calculatedCount
變量是一個(gè)假造的比較消耗性能的計(jì)算表達(dá)式,為了方便顯示性能數(shù)據(jù)打印時(shí)間,我們使用了 IIFE
立即執(zhí)行函數(shù),每次計(jì)算 calculatedCount
都會(huì)輸出它的計(jì)算消耗時(shí)間。
打開(kāi)控制臺(tái),因?yàn)槭?IIFE
,所以首次會(huì)直接打印出時(shí)間。然后,再點(diǎn)擊 +
號(hào),會(huì)發(fā)現(xiàn)再次打印出了計(jì)算耗時(shí)。這是因?yàn)?React
組件重渲染的時(shí)候,不僅是 jsx
,而且變量,函數(shù)這種也全部都會(huì)再次聲明一次,因此導(dǎo)致了 calculatedCount
重新執(zhí)行了初始化(計(jì)算),但是這個(gè)變量值并沒(méi)有發(fā)生改變,如果每次渲染都要重新計(jì)算,那也是十分的消耗性能。
注意觀察,在計(jì)算期間,頁(yè)面會(huì)發(fā)生卡死,不能操作,這是 JS 引擎 的機(jī)制,在執(zhí)行任務(wù)的時(shí)候,頁(yè)面永遠(yuǎn)不會(huì)進(jìn)行渲染,直到任務(wù)結(jié)束為止。這個(gè)過(guò)程對(duì)用戶(hù)體驗(yàn)來(lái)說(shuō)是致命的,雖然我們可以通過(guò)微任務(wù)去處理這個(gè)計(jì)算過(guò)程,從而避免頁(yè)面的渲染阻塞,但是消耗性能這個(gè)問(wèn)題仍然存在,我們需要通過(guò)其他方式去解決。
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 計(jì)算耗時(shí):${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ù)雜計(jì)算,并 return 結(jié)果 }, []);
useMemo
返回一個(gè)緩存過(guò)的值,把 "創(chuàng)建" 函數(shù)和依賴(lài)項(xiàng)數(shù)組作為參數(shù)傳入 useMemo
,它僅會(huì)在某個(gè)依賴(lài)項(xiàng)改變時(shí)才重新計(jì)算 memoized
值。這種優(yōu)化有助于避免在每次渲染時(shí)都進(jìn)行高開(kāi)銷(xiāo)的計(jì)算
第一個(gè)參數(shù)是函數(shù),函數(shù)中需要返回計(jì)算值
第二個(gè)參數(shù)是依賴(lài)數(shù)組
- 如果不傳,則每次都會(huì)初始化,緩存失敗
- 如果傳空數(shù)組,則永遠(yuǎn)都會(huì)返回第一次執(zhí)行的結(jié)果
- 如果傳狀態(tài),則在依賴(lài)的狀態(tài)變化時(shí),才會(huì)從新計(jì)算,如果這個(gè)緩存狀態(tài)依賴(lài)了其他狀態(tài)的話,則需要提供進(jìn)去。
這下就很好理解了,我們的 calculatedCount
沒(méi)有任何外部依賴(lài),因此只需要傳遞空數(shù)組作為第二個(gè)參數(shù),開(kāi)始改造
使用
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 計(jì)算耗時(shí):${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 計(jì)算耗時(shí)"的輸出信息永遠(yuǎn)只會(huì)打印一次,因?yàn)樗粺o(wú)限緩存了。
FAQ何時(shí)使用?
當(dāng)你的表達(dá)式十分復(fù)雜需要經(jīng)過(guò)大量計(jì)算的時(shí)候
示例
下面示例中,我們使用狀態(tài)提升,將子組件的 click
事件函數(shù)放在了父組件中,點(diǎn)擊父組件的 +
號(hào),發(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ù)包裹子組件,給緩存起來(lái)
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 />);
但是意外來(lái)了,即使被 memo
包裹的組件,還是被重新渲染了,為什么!
我們來(lái)逐一分析
- 首先,點(diǎn)擊父組件的
+
號(hào),count
發(fā)生變化,于是父組件開(kāi)始重渲染 - 內(nèi)部的未經(jīng)處理的變量和函數(shù)都被重新初始化,
useState
不會(huì)再初始化了, useEffect 鉤子函數(shù)重新執(zhí)行,虛擬 dom 更新 - 執(zhí)行到
Child
組件的時(shí)候,Child
準(zhǔn)備更新,但是因?yàn)樗?memo
緩存組件,于是開(kāi)始淺比較props
參數(shù),到這里為止一切正常 -
Child
組件參數(shù)開(kāi)始逐一比較變更,到了onClick
函數(shù),發(fā)現(xiàn)值為函數(shù),提供的新值也為函數(shù),但是因?yàn)閯倓傇诟附M件內(nèi)部重渲染時(shí)被重新初始化了(生成了新的地址),因?yàn)楹瘮?shù)是引用類(lèi)型值,導(dǎo)致引用地址發(fā)生改變!比較結(jié)果為不相等,React
仍會(huì)認(rèn)為它已更改,因此重新發(fā)生了渲染。
既然函數(shù)重新渲染會(huì)被重新初始化生成新的引用地址,因此我們應(yīng)該避免它重新初始化。這個(gè)時(shí)候,useMemo
的第二個(gè)使用場(chǎng)景就來(lái)了
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ù)通過(guò) useMemo
包裹起來(lái)了,另外函數(shù)永遠(yuǎn)不會(huì)發(fā)生改變,因此傳遞第二參數(shù)為空數(shù)組,再次嘗試點(diǎn)擊 +
號(hào),子組件不會(huì)被重新渲染了。
對(duì)于對(duì)象,數(shù)組,renderProps
(參數(shù)為 react
組件) 等參數(shù),都可以使用 useMemo
進(jìn)行緩存
示例
既然 useMemo
可以緩存變量函數(shù)等,那組件其實(shí)也是一個(gè)函數(shù),能不能被緩存呢?我們?cè)囈辉?/p>
繼續(xù)使用第一個(gè)案例,將 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 />);
嘗試點(diǎn)擊 +
號(hào),是的,Child
被 useMemo
緩存成功了!
小結(jié)
同樣的,不是必要的情況下,和 React.memo
一樣,不需要特別的使用 useMemo
使用場(chǎng)景
- 表達(dá)式有復(fù)雜計(jì)算且不會(huì)頻發(fā)觸發(fā)更新
- 引用類(lèi)型的組件參數(shù),函數(shù),對(duì)象,數(shù)組等(一般情況下對(duì)象和數(shù)組都會(huì)從
useState
初始化,useState
不會(huì)二次執(zhí)行,主要是函數(shù)參數(shù)) -
react
組件的緩存
擴(kuò)展
useCallback
前面使用 useMemo 包裹了函數(shù),會(huì)感覺(jué)代碼結(jié)構(gòu)非常的奇怪
const handleChildClick = useMemo(() => { return () => { // }; }, []);
函數(shù)中又 return
了一個(gè)函數(shù),其實(shí)還有另一個(gè)推薦的 API
, useCallback
來(lái)代替于對(duì)函數(shù)的緩存,兩者功能是完全一樣,只是使用方法的區(qū)別,useMemo
需要從第一個(gè)函數(shù)參數(shù)中 return
出要緩存的函數(shù),useCallback
則直接將函數(shù)傳入第一個(gè)參數(shù)即可
const handleChildClick = useCallback(() => { // }, []);
代碼風(fēng)格上簡(jiǎn)介明了了許多
看完這篇文章,相信你對(duì) React.memo
和 React.useMemo
已經(jīng)有了一定的了解,并且知道何時(shí)/如何使用它們了
原文鏈接:https://juejin.cn/post/7188041140963115066
相關(guān)推薦
- 2023-05-22 PyTorch小功能之TensorDataset解讀_python
- 2023-07-16 uniapp 調(diào)用拍照組件
- 2022-07-26 ubuntu18.04+cuda10.2+tensorrt8.4.1.5配置安裝
- 2023-06-18 Python實(shí)現(xiàn)兩種稀疏矩陣的最小二乘法_python
- 2022-08-13 微信公眾號(hào)--根據(jù)用戶(hù)的opneId發(fā)送模版消息
- 2022-08-22 ?C++?new?和?delete?關(guān)鍵字詳解_C 語(yǔ)言
- 2022-04-03 Python實(shí)現(xiàn)對(duì)相同數(shù)據(jù)分箱的小技巧分享_python
- 2022-07-11 UVM中analysis端口的使用方法
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門(mén)
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支