react如何遍历并比较_[前端进阶] 这可能是最通俗易懂的React 渲染原理及性能优化...
如今的前端,框架橫行,出去面試問到框架是常有的事。
?我比較常用React, 這里就寫了一篇 React 基礎(chǔ)原理的內(nèi)容, 面試基本上也就問這些, 分享給大家。
React 是什么React是一個專注于構(gòu)建用戶界面的 Javascript Library.
一、React做了什么?
Virtual Dom模型
生命周期管理
setState機制
Diff算法
React patch、事件系統(tǒng)
React的 Virtual Dom模型
經(jīng)過 Diff 算法會計算出 Virtual DOM 的差異,然后將這些差異進行實際的DOM操作更新頁面。
二、React ?總體架構(gòu)
三、幾點要了解的知識
JSX?如何生成Element
Element 如何生成DOM
先看一個例子, Counter :
App.js 就做了一件事情,就是把 Counter 組件掛在 #root 上.
Counter 組件里面定義了自己的 state, 這是個默認的 property ,還有一個 handleclick 事件和 一個 render 函數(shù)。
看到 render 這個函數(shù)里,竟然在 JS 里面寫了 html !?
這是一種 JSX 語法。React 為了方便 View 層組件化,承載了構(gòu)建 html 結(jié)構(gòu)化頁面的職責。
這里也簡單的舉個例子:
將 html 語法直接加入到 javascript 代碼中,再通過翻譯器轉(zhuǎn)換到純 javascript 后由瀏覽器執(zhí)行。
這里調(diào)用了 React 和 createElement 方法,這個方法就是用于創(chuàng)建虛擬元素 Virtual Dom 的。
React 把真實的 DOM 樹轉(zhuǎn)換成 Javascript 對象樹,也就是 Virtual Dom。
每次數(shù)據(jù)更新后,重新計算 Virtual Dom ,并和上一次生成的 virtual dom 做對比,對發(fā)生變化的部分做批量更新。
而 React 是通過創(chuàng)建與更新虛擬元素 Virtual Element 來管理整個Virtual Dom 的。
?虛擬元素可以理解為真實元素的對應(yīng),它的構(gòu)建與更新都是在內(nèi)存中完成的,并不會真正渲染到 dom 中去。
回到我們的計數(shù)器 counter 組件:
注意下 a 標簽 createElement 的返回結(jié)果, 這里 CreateElement 只是做了簡單的參數(shù)修正,返回一個 ReactElemet 實例對象。
Virtual element 彼此嵌套和混合,就得到了一顆 virtual dom 的樹:
2Element 如何生成DOM現(xiàn)在我們有了由 ReactElement 組成的 Virtual Dom 樹,接下來我們要怎么我們構(gòu)建好的 Virtual dom tree 渲染到真正的 DOM 里面呢?
這時可以利用 ReactDOM.render 方法,傳入一個 reactElement 和一個 作為容器的 DOM 節(jié)點。
看進去 ReactDOM.render 的源碼,里面有兩個比較關(guān)鍵的步驟:
第一步是 instantiateReactComponent。
這個函數(shù)創(chuàng)建一個 ReactComponent 的實例并返回,也可以看到 ReactDOM.render 最后返回的也是這個實例。
instantiateReactComponent 方法是初始化組件的入口函數(shù),它通過判斷 node 的類型來區(qū)分不同組件的入口。
當 node 為空的時候,初始化空組件。
當 node 為對象,類型 type 字段標記為是字符串,初始化 DOM 標簽。否則初始化自定義組件。
當 node 為字符串或者數(shù)字時,初始化文本組件。
雖然 Component 有多種類型,但是它們具有基本的數(shù)據(jù)結(jié)構(gòu):ReactComponent 類。
注意到這里的 setState, 這也是重點之一。
創(chuàng)建了 Component 實例后,調(diào)用 component 的 mountComponent 方法,注意到這里是會被批量 mount 的,這樣組件就開始進入渲染到 DOM 的流程了。
四、React生命周期
React 組件基本由三個部分組成,
屬性 props
狀態(tài) state
生命周期方法
React 組件可以接受參數(shù)props, 也有自身狀態(tài) state。
一旦接受到的參數(shù) props 或自身狀態(tài) state 有所改變,React 組件就會執(zhí)行相應(yīng)的生命周期方法。React 生命周期的全局圖
首次掛載組件時,按順序執(zhí)行
componentWillMount、
render
componentDidMount
卸載組件時,執(zhí)行 componentDidUnmount
當組件接收到更新狀態(tài),重新渲染組件時,執(zhí)行
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render ?
componentDidUpdate
更新策略
通過 updateComponent 更新組件,首先判讀上下文是否改變,前后元素是否一致,如果不一致且組件的 componentWillReceiveProps 存在,則執(zhí)行。然后進行 state 的合并。
調(diào)用 shouldComponentUpdate 判斷是否需要進行組件更新,如果存在 componentWillUpdate 則執(zhí)行。
后面的流程跟 mountComponent 相似,這里就不贅述了。
五、setState機制
為避免篇幅過長,這部分可移步我的專題文章:
六、Diff算法
Diff算法用于計算出兩個virtual dom的差異,是React中開銷最大的地方。
傳統(tǒng)diff算法通過循環(huán)遞歸對比差異,算法復雜度為 O(n3)。
React diff算法制定了三條策略,將算法復雜度從 O(n3)降低到O(n)。
1. UI中的DOM節(jié)點跨節(jié)點的操作特別少,可以忽略不計。
2. 擁有相同類的組件會擁有相似的DOM結(jié)構(gòu)。擁有不同類的組件會生成不同的DOM結(jié)構(gòu)。
3. 同一層級的子節(jié)點,可以根據(jù)唯一的ID來區(qū)分。
對于策略一,React 對樹進行了分層比較,兩棵樹只會對同一層次的節(jié)點進行比較。
只會對相同層級的 DOM 節(jié)點進行比較,當發(fā)現(xiàn)節(jié)點已經(jīng)不存在時,則該節(jié)點及其子節(jié)點會被完全刪除,不會用于進一步的比較。
如果出現(xiàn)了 DOM 節(jié)點跨層級的移動操作。
如上圖這樣,A節(jié)點就會被直接銷毀了。
Diif 的執(zhí)行情況是:create A -> create C -> create D -> delete A
2.?Element Diff當節(jié)點處于同一層級時,diff 提供了 3 種節(jié)點操作:插入、移動和刪除。
對于同一層的同組子節(jié)點添加唯一 key 進行區(qū)分。
通過 diff 對比后,發(fā)現(xiàn)新舊集合的節(jié)點都是相同的節(jié)點,因此無需進行節(jié)點刪除和創(chuàng)建,只需要將舊集合中節(jié)點的位置更新為新集合中節(jié)點的位置.
七、原理解析
幾個概念
對新集合中的節(jié)點進行循環(huán)遍歷,新舊集合中是否存在相同節(jié)點
nextIndex: 新集合中當前節(jié)點的位置
lastIndex: 訪問過的節(jié)點在舊集合中最右的位置(最大位置)
If (child._mountIndex < lastIndex)
對新集合中的節(jié)點進行循環(huán)遍歷,通過 key 值判斷,新舊集合中是否存在相同節(jié)點,如果存在,則進行移動操作。
在移動操作的過程中,有兩個指針需要注意,
一個是 nextIndex,表示新集合中當前節(jié)點的位置,也就是遍歷新集合時當前節(jié)點的坐標。
另一個是 lastIndex,表示訪問過的節(jié)點在舊集合中最右的位置,
更新流程:
1( 如果新集合中當前訪問的節(jié)點比 lastIndex 大,證明當前訪問節(jié)點在舊集合中比上一個節(jié)點的位置靠后,則該節(jié)點不會影響其他節(jié)點的位置,即不進行移動操作。只有當前訪問節(jié)點比 lastIndex 小的時候,才需要進行移動操作。)
首先,我們開遍歷新集合中的節(jié)點, 當前 lastIndex = 0, nextIndex = 0,拿到了 B.
此時在舊集合中也發(fā)現(xiàn)了 B,B 在舊集合中的 mountIndex 為 1 , 比當前 lastIndex 0 要大,不滿足 child._mountIndex < lastIndex,對 B 不進行移動操作,更新 lastIndex = 1, 訪問過的節(jié)點在舊集合中最右的位置,也就是 B 在舊集合中的位置,nextIndex++ 進入下一步。
2當前 lastIndex = 1, nextIndex = 1,拿到了 A,在舊集合中也發(fā)現(xiàn)了 A,A 在舊集合中的 mountIndex 為 0 , 比當前 lastIndex 1 要小,滿足 child._mountIndex < lastIndex,對 A 進行移動操作,此時 lastIndex 依然 = 1, A 的 _mountIndex 更新為 nextIndex = 1, nextIndex++, 進入下一步.
3這里,A 變成了藍色,表示對 A 進行了移動操作。
當前 lastIndex = 1, nextIndex = 2,拿到了 D,在舊集合中也發(fā)現(xiàn)了 D,D 在舊集合中的 mountIndex 為 3 , 比當前 lastIndex 1 要大,不滿足 child._mountIndex < lastIndex,不進行移動操作,此時 lastIndex = 3, D 的 _mountIndex 更新為 nextIndex = 2, nextIndex++, 進入下一步.
4當前 lastIndex = 3, nextIndex = 3,拿到了 C,在舊集合中也發(fā)現(xiàn)了 C,C 在舊集合中的 mountIndex 為 2 , 比當前 lastIndex 3 要小,滿足 child._mountIndex < lastIndex,要進行移動,此時 lastIndex不變,為 3, C 的 _mountIndex 更新為 nextIndex = 3.
5由于 C 已經(jīng)是最后一個節(jié)點,因此 diff 操作完成.
這樣最后,要進行移動操作的只有 A C。
另一種情況剛剛說的例子是新舊集合中都是相同節(jié)點但是位置不同。
那如果新集合中有新加入的節(jié)點且舊集合存在需要刪除的節(jié)點,
那 diff 又是怎么進行的呢?比如:
1
首先,依舊,我們開遍歷新集合中的節(jié)點, 當前 lastIndex = 0, nextIndex = 0,拿到了 B,此時在舊集合中也發(fā)現(xiàn)了 B,B 在舊集合中的 mountIndex 為 1 , 比當前 lastIndex 0 要大,不滿足 child._mountIndex < lastIndex,對 B 不進行移動操作,更新 lastIndex = 1, 訪問過的節(jié)點在舊集合中最右的位置,也就是 B 在舊集合中的位置,nextIndex++ 進入下一步。
2
當前 lastIndex = 1, nextIndex = 1,拿到了 E,發(fā)現(xiàn)舊集合中并不存在 E,此時創(chuàng)建新節(jié)點 E,nextIndex++,進入下一步
3
當前 lastIndex = 1, nextIndex = 2,拿到了 C,在舊集合中也發(fā)現(xiàn)了 C,C 在舊集合中的 mountIndex 為 2 , 比當前 lastIndex 1 要大,不滿足 child._mountIndex < lastIndex,不進行移動,此時 lastIndex 更新為 2, nextIndex++ ,進入下一步
4
當前 lastIndex = 2, nextIndex = 3,拿到了 A,在舊集合中也發(fā)現(xiàn)了 A,A 在舊集合中的 mountIndex 為 0 , 比當前 lastIndex 2 要小,不滿足 child._mountIndex < lastIndex,進行移動,此時 lastIndex 不變, nextIndex++ ,進入下一步
5
當完成新集合中所有節(jié)點的差異化對比后,還需要對舊集合進行循環(huán)遍歷,判斷是否勛在新集合中沒有但舊集合中存在的節(jié)點。
此時發(fā)現(xiàn)了 D 滿足這樣的情況,因此刪除 D。
Diff 操作完成。
整個過程還是很繁瑣的, 明白過程即可。
二、性能優(yōu)化
由于react中性能主要耗費在于update階段的diff算法,因此性能優(yōu)化也主要針對diff算法。
1減少diff算法觸發(fā)次數(shù)減少diff算法觸發(fā)次數(shù)實際上就是減少update流程的次數(shù)。
正常進入update流程有三種方式:
1、setState
setState機制在正常運行時,由于批更新策略,已經(jīng)降低了update過程的觸發(fā)次數(shù)。
因此,setState優(yōu)化主要在于非批更新階段中(timeout/Promise回調(diào)),減少setState的觸發(fā)次數(shù)。
常見的業(yè)務(wù)場景即處理接口回調(diào)時,無論數(shù)據(jù)處理多么復雜,保證最后只調(diào)用一次setState。
2、父組件render
父組件的render必然會觸發(fā)子組件進入update階段(無論props是否更新)。此時最常用的優(yōu)化方案即為shouldComponentUpdate方法。
最常見的方式為進行this.props和this.state的淺比較來判斷組件是否需要更新。或者直接使用PureComponent,原理一致。
需要注意的是,父組件的render函數(shù)如果寫的不規(guī)范,將會導致上述的策略失效。
// Bad case// 每次父組件觸發(fā)render 將導致傳入的handleClick參數(shù)都是一個全新的匿名函數(shù)引用。
// 如果this.list 一直都是undefined,每次傳入的默認值[]都是一個全新的Array。
// hitSlop的屬性值每次render都會生成一個新對象
class Father extends Component {
onClick() {}
render() {
return <Child handleClick={() => this.onClick()} list={this.list || []} hitSlop={{ top: 10, left: 10}}/>
}
}
// Good case
// 在構(gòu)造函數(shù)中綁定函數(shù),給變量賦值
// render中用到的常量提取成模塊變量或靜態(tài)成員
const hitSlop = {top: 10, left: 10};
class Father extends Component {
constructor(props) {
super(props);
this.onClick = this.onClick.bind(this);
this.list = [];
}
onClick() {}
render() {
return <Child handleClick={this.onClick} list={this.list} hitSlop={hitSlop} />
}
}
3、forceUpdate
forceUpdate方法調(diào)用后將會直接進入componentWillUpdate階段,無法攔截,因此在實際項目中應(yīng)該棄用。
其他優(yōu)化策略
? ?1. ?shouldComponentUpdate
? ? ?使用shouldComponentUpdate鉤子,根據(jù)具體的業(yè)務(wù)狀態(tài),減少不必要的props變化導致的渲染。如一個不用于渲染的props導致的update。
另外, 也要盡量避免在shouldComponentUpdate 中做一些比較復雜的操作, 比如超大數(shù)據(jù)的pick操作等。
2. 合理設(shè)計state,不需要渲染的state,盡量使用實例成員變量。
? ? ?不需要渲染的 props,合理使用 context機制,或公共模塊(比如一個單例服務(wù))變量來替換。
2正確使用 diff算法不使用跨層級移動節(jié)點的操作。
對于條件渲染多個節(jié)點時,盡量采用隱藏等方式切換節(jié)點,而不是替換節(jié)點。
盡量避免將后面的子節(jié)點移動到前面的操作,當節(jié)點數(shù)量較多時,會產(chǎn)生一定的性能問題。
這時一個 List 組件,里面有標題,包含 ListItem 子組件的members列表,和一個按鈕,綁定了一個 onclick 事件.
然后我加了一個插件,可以顯示出各個組件的渲染情況。
現(xiàn)在我們來點擊改變標題, 看看會發(fā)生些什么。
奇怪的事情發(fā)生了,為什么我只改了標題, ?為什么不相關(guān)的 ListItem 組件也會重新渲染呢?
我們可以回到組件生命周期看看為什么。
還記得這個組件更新的生命周期流程圖嘛,這里的重點在于這個 shouldComponentUpdate。
只有這個方法返回 true 的時候,才會進行更新組件的操作。我們進步一來看看源碼。
可以看到這里,原來如果組件沒有定義 shouldComponentUpdate 方法,也是默認認為需要更新的。
當然,我們的 ListItem 組件是沒有定義這個 shouldComponentUpdate 方法的。
然后我們使用PureComponent :
其原理為重新實現(xiàn)了 shouldComponentUpdate 生命周期方法,讓當前傳入的 props 和 state 之前做淺比較,如果返回 false ,那么組件就不會更新了。
這里也放上一張官網(wǎng)的例圖:
根據(jù)渲染流程,首先會判斷shouldComponentUpdate(SCU)是否需要更新。
如果需要更新,則調(diào)用組件的render生成新的虛擬DOM,然后再與舊的虛擬DOM對比(vDOMEq)。
如果對比一致就不更新,如果對比不同,則根據(jù)最小粒度改變?nèi)ジ翫OM;
如果SCU不需要更新,則直接保持不變,同時其子元素也保持不變。
相似的APi還有React.memo:
回到組件
再次回到我們的組件中, 這次點擊按鈕, 把第二條數(shù)據(jù)換掉:
奇怪的事情發(fā)生了,為什么我只改了第二個 listItem, 還是全部 10 個都重新渲染了呢?
原因在于 shallow compare , 淺比較。
前面說到,我們不能直接修改 this.state 的值,所以我們把
this.state.members 拷貝出來再修改第二個人的信息。
很明顯,因為對象的比較是引用地址,顯然是不相等的。
因此 shoudComponentUpdate 方法都返回了 false, 組件就進行了更新。
那么我們怎么能避免這種情況的發(fā)生呢?
其中一個方法是做深比較,但是如果對象或數(shù)組層級比較深和復制,那么這個代價就太昂貴了。
我們就可以用到 Immutable.js 來解決這個問題,進一步提高組件的渲染性能。
?Immutable Data 就是一旦被創(chuàng)建,就是不能再更改的數(shù)據(jù)。
首先,我們定義了一個 Immutable 的 List 對象,List 對應(yīng)于原生 JS 的 Array,對 Immutable 對象進行修改、添加或刪除操作,都會返回一個新的 Immutable 對象,所以這里 bbb 不等于 aaa。
但是同時為了避免深拷貝吧所有節(jié)點都復制一遍帶來的性能消耗,Immutable 使用了結(jié)構(gòu)共享,即如果對象樹中一個節(jié)點發(fā)生變化,只修改這個節(jié)點和受它影響的父節(jié)點,其他節(jié)點則進行共享。
結(jié)果也是我們預期的那樣。
性能分析用好火焰圖, 該優(yōu)化的時候再優(yōu)化。
八、Hooks ?及其后續(xù)更新
為避免篇幅過長,這部分可移步我的專題文章:
最后
總結(jié)
以上是生活随笔為你收集整理的react如何遍历并比较_[前端进阶] 这可能是最通俗易懂的React 渲染原理及性能优化...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 朗科发布Z系列旗舰DDR5内存:6200
- 下一篇: 怎么用计算机打分数,电脑excel中分数