解析React?中的Virtual?DOM
React在前端界一直很流行,而且學起來也不是很難,只需要學會JSX、理解State和Props,然后就可以愉快的玩耍了,但想要成為React的專家你還需要對React有一些更深入的理解,希望本文對你有用。
React在前端界一直很流行,而且學起來也不是很難,只需要學會JSX、理解State
和Props
,然后就可以愉快的玩耍了,但想要成為React的專家你還需要對React有一些更深入的理解,希望本文對你有用。
這是Choerodon的一個前端頁面
在復雜的前端項目中一個頁面可能包含上百個狀態(tài),對React框架理解得更精細一些對前端優(yōu)化很重要。曾經(jīng)這個頁面點擊一條記錄展示詳情會卡頓數(shù)秒,而這僅僅是前端渲染造成的。
為了能夠解決這些問題,開發(fā)者需要了解React組件從定義到在頁面上呈現(xiàn)(然后更新)的整個過程。
React在編寫組件時使用混合HTML
和JavaScript
的一種語法(稱為JSX)。 但是,瀏覽器對JSX及其語法一無所知,瀏覽器只能理解純JavaScript
,因此必須將JSX轉(zhuǎn)換為HTML
。 這是一個div的JSX代碼,它有一個類和一些內(nèi)容:
<div className='cn'> 文本 </div>
在React中將這段jsx變成普通的js之后它就是一個帶有許多參數(shù)的函數(shù)調(diào)用:
React.createElement( 'div', { className: 'cn' }, '文本' );
React.createElement( 'div', { className: 'cn' }, ['Content 1!', React.createElement('br'), 'Content 2!'] ) 它的第一個參數(shù)是一個字符串,對應html中的標簽名,第二個參數(shù)是它的所有屬性所構成的對象,當然,它也有可能是個空對象,剩下的參數(shù)都是這個元素下的子元素,這里的文本也會被當作一個子元素,所以第三個參數(shù)是 `“文本”` 。 到這里你應該就能想象這個元素下有更多`children`的時候會發(fā)生什么。 ```html <div className='cn'> 文本1 <br /> 文本2 </div>
React.createElement( 'div', { className: 'cn' }, '文本1', // 1st child React.createElement('br'), // 2nd child '文本1' // 3rd child )
目前的函數(shù)有五個參數(shù):元素的類型,全部屬性的對象和三個子元素。 由于一個child
也是React已知的HTML
標簽,因此它也將被解釋成函數(shù)調(diào)用。
到目前為止,本文已經(jīng)介紹了兩種類型的child
參數(shù),一種是string
純文本,一種是調(diào)用其他的React.createElement
函數(shù)。其實,其他值也可以作為參數(shù),比如: - 基本類型 false,null,undefined和 true - 數(shù)組 - React組件
使用數(shù)組是因為可以將子組件分組并作為一個參數(shù)傳遞:
當然,React的強大功能不是來自`HTML`規(guī)范中描述的標簽,而是來自用戶創(chuàng)建的組件,例如:
```js function Table({ rows }) { return ( <table> {rows.map(row => ( <tr key={row.id}> <td>{row.title}</td> </tr> ))} </table> ); }
組件允許開發(fā)者將模板分解為可重用的塊。在上面的“純函數(shù)”組件的示例中,組件接受一個包含表行數(shù)據(jù)的對象數(shù)組,并返回React.createElement
對table元素及其行作為子元素的單個調(diào)用 。
每當開發(fā)者將組件放入JSX布局中時它看上去是這樣的:
<Table rows={rows} />
但從瀏覽器角度,它看到的是這樣的:
React.createElement(Table, { rows: rows });
請注意,這次的第一個參數(shù)不是以string
描述的HTML元素,而是組件的引用(即函數(shù)名)。第二個參數(shù)是傳入該組件的props
對象。
將組件放在頁面上
現(xiàn)在,瀏覽器已經(jīng)將所有JSX組件轉(zhuǎn)換為純JavaScript
,現(xiàn)在瀏覽器獲得了一堆函數(shù)調(diào)用,其參數(shù)是其他函數(shù)調(diào)用,還有其他函數(shù)調(diào)用……如何將它們轉(zhuǎn)換為構成網(wǎng)頁的DOM元素?
為此,開發(fā)者需要使用ReactDOM
庫及其render
方法:
function Table({ rows }) { /* ... */ } // 組件定義 // 渲染一個組件 ReactDOM.render( React.createElement(Table, { rows: rows }), // "創(chuàng)建" 一個 component document.getElementById('#root') // 將它放入DOM中 );
當ReactDOM.render
被調(diào)用時,React.createElement
最終也會被調(diào)用,它返回以下對象:
// 這個對象里還有很多其他的字段,但現(xiàn)在對開發(fā)者來說重要的是這些。 { type: Table, props: { rows: rows }, // ... }
這些對象構成了React意義上的Virtual DOM
它們將在所有進一步渲染中相互比較,并最終轉(zhuǎn)換為真正的DOM(與Virtual DOM對比)。
這是另一個例子:這次有一個div具有class屬性和幾個子節(jié)點:
React.createElement( 'div', { className: 'cn' }, 'Content 1!', 'Content 2!', );
變成:
{ type: 'div', props: { className: 'cn', children: [ 'Content 1!', 'Content 2!' ] } }
所有的傳入的展開函數(shù),也就是React.createElement
除了第一第二個參數(shù)剩下的參數(shù)都會在props
對象中的children
屬性中,不管傳入的是什么函數(shù),他們最終都會作為children
傳入props
中。
而且,開發(fā)者可以直接在JSX代碼中添加children
屬性,將子項直接放在children
中,結果仍然是相同的:
<div className='cn' children={['Content 1!', 'Content 2!']} />
在Virtual DOM對象被建立出來之后ReactDOM.render
會嘗試按以下規(guī)則把它翻譯成瀏覽器能夠看得懂的DOM節(jié)點: -如果
Virtual DOM對象中的type屬性是一個string類型的tag名稱,就
創(chuàng)建一個tag,包含props里的全部屬性。 -如果
Virtual DOM對象中的type屬性是一個函數(shù)或者class,就
調(diào)用它,它返回的可能還是一個Virtual DOM然后將結果繼續(xù)遞歸調(diào)用此過程。 -如果
props中有children屬性,就
對children中的每個元素進行以上過程,并將返回的結果放到父DOM節(jié)點中。
最后,瀏覽器獲得了以下HTML(對于上述table的例子):
<table> <tr> <td>Title</td> </tr> ... </table>
重建DOM
接下瀏覽器要“重建”一個DOM節(jié)點,如果瀏覽器要更新一個頁面,顯然,開發(fā)者并不希望替換頁面中的全部元素,這就是React真正的魔法了。如何才能實現(xiàn)它?先從最簡單的方法開始,重新調(diào)用這個節(jié)點的ReactDOM.render
方法。
// 第二次調(diào)用 ReactDOM.render( React.createElement(Table, { rows: rows }), document.getElementById('#root') );
這一次,上面的代碼執(zhí)行邏輯將與看到的代碼不同。React不是從頭開始創(chuàng)建所有DOM節(jié)點并將它們放在頁面上,React將使用“diff”算法,以確定節(jié)點樹的哪些部分必須更新,哪些部分可以保持不變。
那么它是怎樣工作的?只有少數(shù)幾個簡單的情況,理解它們將對React程序的優(yōu)化有很大幫助。請記住,接下來看到的對象是用作表示React Virtual DOM中節(jié)點的對象。
▌Case 1:type是一個字符串,type在調(diào)用之間保持不變,props也沒有改變。
// before update { type: 'div', props: { className: 'cn' } } // after update { type: 'div', props: { className: 'cn' } }
這是最簡單的情況:DOM保持不變。
▌Case 2:type仍然是相同的字符串,props是不同的。
// before update: { type: 'div', props: { className: 'cn' } } // after update: { type: 'div', props: { className: 'cnn' } }
由于type仍然代表一個HTML元素,React知道如何通過標準的DOM API調(diào)用更改其屬性,而無需從DOM樹中刪除節(jié)點。
▌Case 3:type已更改為不同的組件String或從String組件更改為組件。
// before update: { type: 'div', props: { className: 'cn' } } // after update: { type: 'span', props: { className: 'cn' } }
由于React現(xiàn)在看到類型不同,它甚至不會嘗試更新DOM節(jié)點:舊元素將與其所有子節(jié)點一起被刪除(unmount)。因此,在DOM樹上替換完全不同的元素的代價會非常之高。幸運的是,這在實際情況中很少發(fā)生。
重要的是要記住React使用===(三等)來比較type值,因此它們必須是同一個類或相同函數(shù)的相同實例。
下一個場景更有趣,因為這是開發(fā)者最常使用React的方式。
▌Case 4:type是一個組件。
// before update: { type: Table, props: { rows: rows } } // after update: { type: Table, props: { rows: rows } }
你可能會說,“這好像沒有任何變化”,但這是不對的。
如果type是對函數(shù)或類的引用(即常規(guī)React組件),并且啟動了樹diff比較過程,那么React將始終嘗試查看組件內(nèi)部的所有child
以確保render
的返回值沒有更改。即在樹下比較每個組件 - 是的,復雜的渲染也可能變得昂貴!
組件中的children
除了上面描述的四種常見場景之外,當元素有多個子元素時,開發(fā)者還需要考慮React的行為。假設有這樣一個元素:
// ... props: { children: [ { type: 'div' }, { type: 'span' }, { type: 'br' } ] }, // ...
開發(fā)者開發(fā)者想將它重新渲染成這樣(span
和div
交換了位置):
// ... props: { children: [ { type: 'span' }, { type: 'div' }, { type: 'br' } ] }, // ...
那么會發(fā)生什么?
當React看到里面的任何數(shù)組類型的props.children
,它會開始將它中的元素與之前看到的數(shù)組中的元素按順序進行比較:index 0將與index 0,index 1與index 1進行比較,對于每對子元素,React將應用上述規(guī)則集進行比較更新。在以上的例子中,它看到div
變成一個span
這是一個情景3中的情況。但這有一個問題:假設開發(fā)者想要從1000行表中刪除第一行。React必須“更新”剩余的999個孩子,因為如果與先前的逐個索引表示相比,他們的內(nèi)容現(xiàn)在將不相等。
幸運的是,React有一種內(nèi)置的方法來解決這個問題。如果元素具有key
屬性,則元素將通過key
而不是索引進行比較。只要key
是唯一的,React就會移動元素而不將它們從DOM樹中移除,然后將它們放回(React中稱為掛載/卸載的過程)。
// ... props: { children: [ // 現(xiàn)在react就是根據(jù)key,而不是索引來比較了 { type: 'div', key: 'div' }, { type: 'span', key: 'span' }, { type: 'br', key: 'bt' } ] }, // ...
當狀態(tài)改變時
到目前為止,本文只觸及了props
,React哲學的一部分,但忽略了state
。這是一個簡單的“有狀態(tài)”組件:
class App extends Component { state = { counter: 0 } increment = () => this.setState({ counter: this.state.counter + 1, }) render = () => (<button onClick={this.increment}> {'Counter: ' + this.state.counter} </button>) }
現(xiàn)在,上述例子中的state
對象有一個counter
屬性。單擊按鈕會增加其值并更改按鈕文本。但是當用戶點擊時,DOM會發(fā)生什么?它的哪一部分將被重新計算和更新?
調(diào)用this.setState
也會導致重新渲染,但不會導致整個頁面重渲染,而只會導致組件本身及其子項。父母和兄弟姐妹都可以幸免于難。
修復問題
本文準備了一個DEMO,這是修復問題前的樣子。你可以在這里查看其源代碼。不過在此之前,你還需要安裝React Developer Tools。
打開demo要看的第一件事是哪些元素以及何時導致Virtual DOM更新。導航到瀏覽器的Dev Tools中的React面板,點擊設置然后選擇“Highlight Updates”復選框:
現(xiàn)在嘗試在表中添加一行。如你所見,頁面上的每個元素周圍都會出現(xiàn)邊框。這意味著每次添加行時,React都會計算并比較整個Virtual DOM樹?,F(xiàn)在嘗試按一行內(nèi)的計數(shù)器按鈕。你將看到Virtual DOM如何更新 (state僅相關元素及其子元素更新)。
React DevTools暗示了問題可能出現(xiàn)的地方,但沒有告訴開發(fā)者任何細節(jié):特別是有問題的更新是指元素“diff”之后有不同,還是組件被unmount/mount了。要了解更多信息,開發(fā)者需要使用React的內(nèi)置分析器(請注意,它不能在生產(chǎn)模式下工作)。
轉(zhuǎn)到Chrome DevTools中的“Performance”標簽。點擊record按鈕,然后點擊表格。添加一些行,更改一些計數(shù)器,然后點擊“Stop”按鈕。稍等一會兒之后開發(fā)者會看到:
在結果輸出中,開發(fā)者需要關注“Timing”??s放時間軸,直到看到“React Tree Reconciliation”組及其子項。這些都是組件的名稱,旁邊有[update]或[mount]??梢钥吹接幸粋€TableRow被mount了,其他所有的TableRow都在update,這并不是開發(fā)者想要的。
大多數(shù)性能問題都由[update]或[mount]引起
一個組件(以及組件下的所有東西)由于某種原因在每次更新時重新掛載,開發(fā)者不想讓它發(fā)生(重新掛載很慢),或者在大型分支上執(zhí)行代價過大的重繪,即使組件似乎沒有發(fā)生任何改變。
修復mount/unmount
現(xiàn)在,當開發(fā)者了解React如何決定更新Virtual DOM并知道幕后發(fā)生的事情時,終于準備好解決問題了!修復性能問題首先要解決 mount/unmount。
如果開發(fā)者將任何元素/組件的多個子元素在內(nèi)部表示為數(shù)組,那么程序可以獲得非常明顯的速度提升。
考慮一下:
<div> <Message /> <Table /> <Footer /> </div>
在虛擬DOM中,將表示為:
// ... props: { children: [ { type: Message }, { type: Table }, { type: Footer } ] } // ...
一個簡單的Message
組件(是一個div
帶有一些文本,像是豬齒魚的頂部通知)和一個很長的Table
,比方說1000多行。它們都是div
元素的child
,因此它們被放置在父節(jié)點的props.children
之下,并且它們沒有key
。React甚至不會通過控制臺警告來提醒開發(fā)者分配key,因為子節(jié)點React.createElement
作為參數(shù)列表而不是數(shù)組傳遞給父節(jié)點。
現(xiàn)在,用戶已經(jīng)關閉了頂部通知,所以Message
從樹中刪除。Table
、Footer
是剩下的child。
// ... props: { children: [ { type: Table }, { type: Footer } ] } // ...
React如何看待它?它將它視為一系列改變了type的child:children[0]的type本來是Message
,但現(xiàn)在他是Table
。因為它們都是對函數(shù)(和不同函數(shù))的引用,它會卸載整個Table并再次安裝它,渲染它的所有子代:1000多行!
因此,你可以添加唯一鍵(但在這種特殊情況下使用key
不是最佳選擇)或者采用更智能的trick:使用 && 的布爾短路運算,這是JavaScript
和許多其他現(xiàn)代語言的一個特性。像這樣:
<div> {isShowMessage && <Message />} <Table /> <Footer /> </div>
即使Message
被關閉了(不再顯示),props.children
父母div仍將擁有三個元素,children[0]具有一個值false
(布爾類型)。還記得true
/false
,null
甚至undefined
都是Virtual DOM對象type屬性的允許值嗎?瀏覽器最終得到類似這樣的東西:
// ... props: { children: [ false, // isShowMessage && <Message /> 短路成了false { type: Table }, { type: Footer } ] } // ...
所以,不管Message
是否被顯示,索引都不會改變,Table
仍然會和Table
比較,但僅僅比較Virtual DOM通常比刪除DOM節(jié)點并從中創(chuàng)建它們要快得多。
現(xiàn)在來看看更高級的東西。開發(fā)者喜歡HOC。高階組件是一個函數(shù),它將一個組件作為一個參數(shù),添加一些行為,并返回一個不同的組件(函數(shù)):
function withName(SomeComponent) { return function(props) { return <SomeComponent {...props} name={name} />; } }
開發(fā)者在父render
方法中創(chuàng)建了一個HOC 。當React
需要重新渲染樹時,React
的Virtual DOM將如下所示:
// On first render: { type: ComponentWithName, props: {}, } // On second render: { type: ComponentWithName, // Same name, but different instance props: {}, }
現(xiàn)在,React只會在ComponentWithName上運行一個diff算法,但是這次同名引用了一個不同的實例,三等于比較失敗,必須進行完全重新掛載。注意它也會導致狀態(tài)丟失,幸運的是,它很容易修復:只要返回的實例都是同一個就好了:
// 單例 const ComponentWithName = withName(Component); class App extends React.Component() { render() { return <ComponentWithName />; } }
修復update
現(xiàn)在瀏覽器已經(jīng)確保不會重新裝載東西了,除非必要。但是,對位于DOM樹根目錄附近的組件所做的任何更改都將導致其所有子項的進行對比重繪。結構復雜,價格昂貴且經(jīng)??梢员苊?。
如果有辦法告訴React不要查看某個分支,那將是很好的,因為它沒有任何變化。
這種方式存在,它涉及一個叫shouldComponentUpdate
的組件生命周期函數(shù)。React會在每次調(diào)用組件之前調(diào)用此方法,并接收props
和state
的新值。然后開發(fā)者可以自由地比較新值和舊值之間的區(qū)別,并決定是否應該更新組件(返回true
或false
)。如果函數(shù)返回false
,React將不會重新渲染有問題的組件,也不會查看其子組件。
通常比較兩組props
和state
一個簡單的淺層比較就足夠了:如果頂層屬性的值相同,瀏覽器就不必更新了。淺比較不是JavaScript
的一個特性,但開發(fā)者很多方法來自己實現(xiàn)它,為了不重復造輪子,也可以使用別人寫好的方法。
在引入淺層比較的npm包后,開發(fā)者可以編寫如下代碼:
class TableRow extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { props, state } = this; return !shallowequal(props, nextProps) && !shallowequal(state, nextState); } render() { /* ... */ } }
但是你甚至不必自己編寫代碼,因為React在一個名為React.PureComponent的類中內(nèi)置了這個功能,它類似于React.Component
,只是shouldComponentUpdate
已經(jīng)為你實現(xiàn)了淺層props/state比較。
或許你會有這樣的想法,能替換Component
為PureComponent
就去替換。但開發(fā)者如果錯誤地使用PureComponent
同樣會有重新渲染的問題存在,需要考慮下面三種情況:
<Table // map每次都會返回一個新的數(shù)組實例,所以每次比較都是不同的 rows={rows.map(/* ... */)} // 每一次傳入的對象都是新的對象,引用是不同的。 style={ { color: 'red' } } // 箭頭函數(shù)也一樣,每次都是不同的引用。 onUpdate={() => { /* ... */ }} />
上面的代碼片段演示了三種最常見的反模式,請盡量避免它們!
正確地使用PureComponent
,你可以在這里看到所有的TableRow都被“純化”后渲染的效果。
但是,如果你迫不及待想要全部使用純函數(shù)組件,這樣是不對的。比較兩組props
和state
不是免費的,對于大多數(shù)基本組件來說甚至都不值得:運行shallowCompare
比diff算法需要更多時間。
可以使用此經(jīng)驗法則:純組件適用于復雜的表單和表格,但它們通常會使按鈕或圖標等簡單元素變慢。
現(xiàn)在,你已經(jīng)熟悉了React的渲染模式,接下來就開始前端優(yōu)化之旅吧。
到此這篇關于React 的 Virtual DOM的文章就介紹到這了,更多相關React 的 Virtual DOM內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
遠程連接局域網(wǎng)內(nèi)的sql server 無法連接 錯誤與解決方法
下面我們依次介紹如何來解決這三個最常見的連接錯誤。2009-09-09如何恢復數(shù)據(jù)庫備份到一個已存在的正在使用的數(shù)據(jù)庫上
如何恢復數(shù)據(jù)庫備份到一個已存在的正在使用的數(shù)據(jù)庫上...2007-01-01SQL SERVER 2014 安裝圖解教程(含SQL SERVER 2014下載)
這篇文章主要介紹了SQL SERVER 2014 安裝圖解教程(含SQL SERVER 2014下載),需要的朋友可以參考下2015-10-10sqlserver 自動備份所有數(shù)據(jù)庫的SQL
可自動備份除系統(tǒng)數(shù)據(jù)庫外的所有數(shù)據(jù)庫。備份文件的周期保存周期可以更改。2010-03-03SQL Server簡單模式下誤刪除堆表記錄恢復方法(繞過頁眉校驗)
這篇主旨是揭示堆表的刪除記錄找回的原理,我所考慮的方面并不適用于每個人的每種情況,望大家見諒2013-01-01