React重新渲染超詳細(xì)講解
Web 前端開發(fā)者對(duì)渲染和重新渲染應(yīng)該不陌生,在 React 中,它們究竟是什么意思?
- 渲染:React 讓組件根據(jù)當(dāng)前的 props 和 state 描述它要展示的內(nèi)容。
- 重新渲染:React 讓組件重新描述它要展示的內(nèi)容。
要將組件顯示到屏幕上,React 的工作主要分為兩個(gè)階段,本文介紹與 React 渲染相關(guān)的知識(shí)。
- render 階段(渲染階段):計(jì)算組件的輸出并收集所有需要應(yīng)用到 DOM 上的變更。
- commit 階段(提交階段):將 render 階段計(jì)算出的變更應(yīng)用到 DOM 上。
在 commit 階段 React 會(huì)更新 DOM 節(jié)點(diǎn)和組件實(shí)例的 ref,如果是類組件,React會(huì)同步運(yùn)行 componentDidMount 或 componentDidUpdate 生命周期方法,如果是函數(shù)組件,React會(huì)同步運(yùn)行 useLayoutEffect 勾子,當(dāng)瀏覽器繪制 DOM 之后,再運(yùn)行所有的 useEffect 勾子。
React 重新渲染
初始化渲染之后,下面的這些原因會(huì)讓React重新渲染組件:
類組件
- 調(diào)用 this.setState 方法。
- 調(diào)用this.forceUpdate方法。
函數(shù)組件
- 調(diào)用 useState 返回的 setState。
- 調(diào)用 useReducer 返回的 dispatch。
其他
- 組件訂閱的 context value 發(fā)生變更
- 重新調(diào)用 ReactDOM.render(
<AppRoot>
)
假設(shè)組件樹如下
默認(rèn)情況,如果父組件重新渲染,那么 React 會(huì)重新渲染它所有的子組件。當(dāng)用戶點(diǎn)擊組件 A 中的按鈕,使 A 組件 count 狀態(tài)值加1,將發(fā)生如下的渲染流程:
- React將組件A添加到重新渲染隊(duì)列中。
- 從組件樹的頂部開始遍歷,快速跳過(guò)不需要更新的組件。
- React發(fā)生A組件需要更新,它會(huì)渲染A。A返回B和C
- B沒(méi)有被標(biāo)記為需要更新,但由于它的父組件A被渲染了,所以React會(huì)渲染B
- C沒(méi)有被標(biāo)記為需要更新,但由于它的父組件A被渲染了,所以React會(huì)渲染C,C返回D
- D沒(méi)有標(biāo)記為需要更新,但由于它的父組件C被渲染了,所以D會(huì)被渲染。
在默認(rèn)渲染流程中,React 不關(guān)心子組件的 props 是否改變了,它會(huì)無(wú)條件地渲染子組件。很可能上圖中大多數(shù)組件會(huì)返回與上次完全相同的結(jié)果,因此 React 不需要對(duì)DOM 做任何更改,但是,React 仍然會(huì)要求組件渲染自己并對(duì)比前后兩次渲染輸出的結(jié)果,這兩者都需要時(shí)間。
Reconciliation
Reconciliation 被稱為 diff 算法,它用來(lái)比較兩顆 React 元素樹之間的差異,為了讓組件重新渲染變得高效,React 盡可能地復(fù)用現(xiàn)有的組件和 DOM。為了降低時(shí)間復(fù)雜度,Diff 算法基于如下兩個(gè)假設(shè):
- 兩個(gè)不同類型的元素對(duì)應(yīng)的元素樹完全不同。
- 在同一個(gè)列表中,如果兩個(gè)元素key屬性的值相同,那么它們被識(shí)別為同一個(gè)元素。
元素類型對(duì) Diff 的影響
React 使用元素的 type 字段比較元素類型是否相同,如果兩顆樹在相同位置要渲染的元素類型相同,那么 React 就重用這些元素,并在適當(dāng)?shù)臅r(shí)候更新,不需要重新創(chuàng)建元素,這意味著,只要一直要求 React 將某組件渲染在相同的位置,那么 React 始終不會(huì)卸載該組件。如果相同位置的元素類型不同,例如從 div 到 span 或者從ComponentA 到 ComponentB,React會(huì)認(rèn)為整個(gè)樹發(fā)生了變化,為了加快比較過(guò)程,React 會(huì)銷毀整個(gè)現(xiàn)有的組件樹,包括所有的 DOM 節(jié)點(diǎn),然后重新創(chuàng)建元素。
瀏覽器內(nèi)置元素的 type 字段是一個(gè)字符串,自定義組件元素的 type 字段是一個(gè)類或者函數(shù),由于元素類型對(duì) Diff的影響,所以在渲染期間不要?jiǎng)?chuàng)建組件,只要?jiǎng)?chuàng)建一個(gè)新的組件,那么它的 type 字段就是不同的引用,這將導(dǎo)致 React 不斷地銷毀并重新創(chuàng)建子組件樹。不要有如下的代碼:
function ParentCom() { // 每一次渲染 ParentCom 時(shí),都會(huì)創(chuàng)建新的ChildCom組件 function ChildCom() {/**do something*/} return <ChildCom /> }
上述代碼不推薦,正確的做法是將 ChildCom 放在ParentCom 的外面。
key 對(duì) Diff 的影響
React 識(shí)別元素的另一種方式是通過(guò) key 屬性,key 作為組件的唯一標(biāo)識(shí)符不會(huì)當(dāng)作prop傳遞到組件中,可以給任何組件添加一個(gè) key 屬性來(lái)標(biāo)注它,更改 key 的值會(huì)導(dǎo)致舊的組件實(shí)例和 DOM 被銷毀。
列表是使用 key 屬性的主要場(chǎng)景,在 React 官方文檔中提到,不要將數(shù)組的下標(biāo)作為 key 值,而是用數(shù)據(jù)唯一 ID 作為 key 值。在這里分別介紹這兩種方式的區(qū)別。
假如 Todo List 中有 10 項(xiàng),先用數(shù)組下標(biāo)作為 key 的值,這 10 項(xiàng) Todo 的 key 值為 0...9,現(xiàn)在刪除數(shù)組的第 6 項(xiàng)和第 7 項(xiàng),并在數(shù)組末尾添加 3 個(gè)新的數(shù)據(jù)項(xiàng),我們最終將得到 key 值為0..10的 Todo,看起來(lái)只是在末尾新增 1 項(xiàng),將原來(lái)的列表從10項(xiàng)變成了11項(xiàng),React 很樂(lè)意復(fù)用已有的 DOM 節(jié)點(diǎn)和組件實(shí)例,這意味著原來(lái) #6 對(duì)應(yīng)的組件實(shí)例沒(méi)有被銷毀,現(xiàn)在它接收新的 props 用于呈現(xiàn)原來(lái)的 #8。在這個(gè)例子中 React 會(huì)創(chuàng)建 1 個(gè)Todo,更新 4 個(gè)Todo。
如果使用數(shù)據(jù)的 ID 作為 key 值,React 能發(fā)現(xiàn)第 6 項(xiàng)和第 7 項(xiàng)被刪除了,它也能發(fā)現(xiàn)數(shù)組新增了 3 項(xiàng),所以 React 會(huì)銷毀 #6 和 #7 項(xiàng)對(duì)應(yīng)的組件實(shí)例及其關(guān)聯(lián)的 DOM,還會(huì)創(chuàng)建 3 個(gè)組件實(shí)例及其關(guān)聯(lián)的 DOM。
提高渲染性能
要將組件顯示在界面上,組件必須經(jīng)歷渲染流程,但渲染工作有時(shí)候會(huì)被認(rèn)為是浪費(fèi)時(shí)間,如果渲染的輸出結(jié)果沒(méi)有改變,它對(duì)應(yīng)的DOM節(jié)點(diǎn)也不需要更新,此時(shí)與該組件相關(guān)的渲染工作真的是在浪費(fèi)時(shí)間。React組件的輸出結(jié)果始終基于當(dāng)前 props 和 state 的值,因此,如果我們知道組件的 props 和 state 沒(méi)有改變,那么我們可以無(wú)后顧之憂地讓組件跳過(guò)重新渲染。
跳過(guò)重新渲染
React 提供了 3 個(gè)主要的API讓我們跳過(guò)重新渲染:
- React.Component 的 shouldComponentUpdate:這是類組件可選的生命周期函數(shù),它在組件 render 階段早期被調(diào)用,如果返回false,React 將跳過(guò)重新渲染該組件,使用它最常見的場(chǎng)景是檢查組件的 props 和 state 是否自上次以來(lái)發(fā)生了變更,如果沒(méi)有改變則返回false。
- React.PureComponent:它在 React.Component 的基礎(chǔ)上添加默認(rèn)的 shouldComponentUpdate 去比較組件的 props 和 state 自上次渲染以來(lái)是否有變更。
- React.memo():它是一個(gè)高階組件,接收自定義組件作為參數(shù),返回一個(gè)被包裹的組件,被包裹的組件的默認(rèn)行為是檢查 props 是否有更改,如果沒(méi)有,則跳過(guò)重新渲染。
上述方法都通過(guò)‘淺比較’來(lái)確定值是否有變更,如果通過(guò) mutable 的方式修改狀態(tài),這些 API 會(huì)認(rèn)為狀態(tài)沒(méi)有變。
- 如果組件在其渲染過(guò)程中返回的元素的引用與上一次渲染時(shí)的引用完全相同,那么 React 不會(huì)重新渲染引用相同的組件。示例如下:
function ShowChildren(props: {children: React.ReactNode}) { const [count, setCount] = useState<number>(0) return ( <div> {count} <button onClick={() => setCount(c => c + 1)}>click</button> {/* 寫法一 */} {props.children} {/* 寫法二 */} {/* <Children/> */} </div> ) }
上述 ShowChildren 的 props.children 對(duì)應(yīng) Children 組件,因此寫法一和寫法二在瀏覽器中呈現(xiàn)一樣。點(diǎn)擊按鈕不會(huì)讓寫法一的 Children 組件重新渲染,但是會(huì)使寫法二的 Children 組件重新渲染。
上述4種方式跳過(guò)重新渲染意味著 React 會(huì)跳過(guò)整個(gè)子樹的重新渲染。
Props 對(duì)渲染優(yōu)化的影響
默認(rèn)情況,只要組件重新渲染,React 會(huì)重新渲染所有被它嵌套的后代組件,即便組件的 props 沒(méi)有變更。如果試圖通過(guò) React.memo 和 React.PureComponent 優(yōu)化組件的渲染性能,那么要注意每個(gè) prop 的引用是否有變更。下面的示例試圖使用 React.memo 讓組件不重新渲染,但事與愿違,組件會(huì)重新渲染,代碼如下:
const MemoizedChildren = React.memo(Children) function Parent() { const onClick = () => { /** todo*/} return <MemoizedChildren onClick={onClick}/> }
上述代碼中,Parent 組件重新渲染會(huì)創(chuàng)建新的 onClick 函數(shù),所以對(duì) MemoizedChildren 而言,props.onClic k的引用有變化,最終被 React.memo 包裹的Children 會(huì)重新渲染,如果讓組件跳過(guò)重新渲染對(duì)你真的很重要,那么在上述代碼中將 React.memo 與 useCallback 配合使用才能達(dá)到目的。
總結(jié)
渲染與更新 DOM 是不同的事情,組件經(jīng)歷了渲染,DOM 不一定會(huì)更新,如果渲染組件返回的結(jié)果與上次的相同,那么它的 DOM 節(jié)點(diǎn)不需要有任何更新。與 React 渲染密切相關(guān)的還有另一個(gè)概念,即Immutability,在React 狀態(tài)的不變性一文已介紹過(guò)它。
到此這篇關(guān)于React重新渲染超詳細(xì)講解的文章就介紹到這了,更多相關(guān)React重新渲染內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
react-native 配置@符號(hào)絕對(duì)路徑配置和絕對(duì)路徑?jīng)]有提示的問(wèn)題
本文主要介紹了react-native 配置@符號(hào)絕對(duì)路徑配置和絕對(duì)路徑?jīng)]有提示的問(wèn)題,文中通過(guò)圖文示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-01-01React模擬實(shí)現(xiàn)Vue的keepAlive功能
Vue中,keep-alive組件可以緩存組件狀態(tài),在路由切換時(shí)重新掛載,實(shí)現(xiàn)這一功能在React中并不簡(jiǎn)單,但我們可以借助一個(gè)第三方庫(kù)——react-activation 來(lái)模擬Vue的keep-alive功能,需要的朋友可以參考下2024-10-10使用useImperativeHandle時(shí)父組件第一次沒(méi)拿到子組件的問(wèn)題
這篇文章主要介紹了使用useImperativeHandle時(shí)父組件第一次沒(méi)拿到子組件的問(wèn)題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08使用Ant Design Anchor組件的一個(gè)坑及解決
這篇文章主要介紹了使用Ant Design Anchor組件的一個(gè)坑及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-04-04