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