前端優化反應:虛擬dom解釋

恍惚的二狗發表於2018-06-18

瞭解反應的虛擬dom,並使用此知識加快應用程式。在這個全面入門的框架內部入門中,我們將揭開JSX的神祕化,讓您展示如何做出反應,解釋如何找到瓶頸,並分享一些避免常見錯誤的提示。

反應的原因之一一直動搖著前端世界,並沒有下降的跡象,它平易近人的學習曲線:在你繞著頭後,學習曲線。n.JSX還有整個“國家vs.道具“概念,你可以走了。

如果你已經熟悉了反應工作的方式,你可以直接跳到“修理東西”.

但是要真正掌握自己的反應,你需要思考反應。這篇文章是想幫你解決這個問題。看看所做的反應表我們的專案之一:

A huge React table

一個巨大的反應表ebay業務.

使用數百條動態的、多層的行,理解框架的細點對於保證使用者體驗的順利進行至關重要。

當事情發生的時候你肯定會感覺到。輸入欄位會得到laggy,核取方塊會先檢查一下,情態動詞會出現困難的時候。

為了解決這些問題,我們需要覆蓋整個旅程,一個反應元件從定義到您定義(然後更新)頁面上。繫好安全帶!

在JSX後面

過程中已知的前端圈為“transpiling”,即使“編譯”將是一個更正確的術語。

反應開發人員敦促您在編寫元件時使用名為JSX的html和javascript組合。然而,瀏覽器對於JSX及其語法沒有任何線索。瀏覽器只理解簡單javascript,所以必須將JSX轉換成它。下面是一個div它有一個類和一些內容:

<div className=`cn`>
 Content!
</div> 

“正式”javascript中的相同程式碼只是一個帶有若干引數的函式呼叫:

React.createElement(
 `div`,
 { className: `cn` },
 `Content!`
);

讓我們仔細看看這些論點。二是一個元素型別。對於html標記,它將是一個帶有標記名的字串。第二個引數是一個物件,它包含所有元素屬性。如果沒有空物件,它也可以是一個空物件。下面所有的論點都是元素的孩子。元素中的文字也作為子元素計數,因此字串“內容!”作為函式呼叫的第三個引數放置。

你已經可以想象當我們有更多孩子時會發生什麼:

<div className=`cn`>
 Content 1!
 <br />
 Content 2!
</div> 
React.createElement(
 `div`,
 { className: `cn` },
 `Content 1!`, // 1st child
 React.createElement(`br`), // 2nd child `Content 2!` // 3rd child
)

我們的函式現在有五個引數:元素型別、屬性物件和三個子元素。因為我們的一個孩子也是一個眾所周知的反應,它將被描繪成一個函式呼叫。

現在,我們已經覆蓋了兩種型別的兒童:平原String或者另一個電話React.createElement。然而,其他價值也可以作為論據:

  • 基元false, null, undefined以及true
  • 陣列
  • 反應元件

陣列是用來作為一個引數分組並傳遞的子元素:

React.createElement(
 `div`,
 { className: `cn` },
 [`Content 1!`, React.createElement(`br`), `Content 2!`]
)

當然,反應的力量來自於html規範中描述的標籤,但是來自使用者建立的元件,例如:

function Table({ rows }) {
 return (
 <table>
 {rows.map(row => (
 <tr key={row.id}> <td>{row.title}</td> </tr>
 ))}
 </table>
 );
}

元件允許我們將模板破壞成可重用的塊。在一個示例中,“功能”上面的元件接受陣列行資料的物件陣列,並返回單個React.createElement請呼叫<table>元素及其行作為子。

每當我們把元件放置到這樣的佈局中:

<Table rows={rows} /> 

從瀏覽器的角度來看,我們寫了這篇文章:

 React.createElement(Table, { rows: rows });

注意,這次我們的第一個引數不是String描述一個html元素,但是對我們定義的函式的引用當我們編碼我們的元件時。我們的屬性現在是我們的props.

將元件放在頁面上

所以,我們已經將所有的JSX元件都轉換成純javascript,現在我們有了一系列函式呼叫,其中還有其他函式呼叫,還有其他函式呼叫…如何將它們轉換成構成web頁面的dom元素?

為了這個,我們有一個ReactDOM圖書館及其render方法:

function Table({ rows }) { /* ... */ } // defining a component // rendering a component
ReactDOM.render(
 React.createElement(Table, { rows: rows }), // "creating" a component document.getElementById(`#root`) // inserting it on a page
);

何時ReactDOM.render被稱為React.createElement最後呼叫了它,它返回以下物件:

// There are more fields, but these are most important to us
{
 type: Table,
 props: {
 rows: rows
 },
 // ...
}

這些物件構成了虛擬dom在反應上的意義。

它們將在所有進一步渲染中相互比較,最終轉換為dom(與虛擬).

下面是另一個例子:這次使用div具有類屬性和若干子屬性:

React.createElement(
 `div`,
 { className: `cn` },
 `Content 1!`,
 `Content 2!`,
);

變成:

{
 type: `div`,
 props: {
 className: `cn`,
 children: [
 `Content 1!`,
 `Content 2!`
 ]
 }
}

注意,過去使用的是單獨的引數React.createElement函式在a/s之下找到了它們的位置children內鑰匙props。所以它無所謂如果孩子作為陣列或引數列表傳遞-在生成的虛擬dom物件中,它們最終都會一起結束。

此外,我們可以將孩子直接新增到在程式碼中,結果仍然是一樣的:

<div className=`cn` children={[`Content 1!`, `Content 2!`]} />

構建了虛擬dom物件之後,ReactDOM.render將嘗試將其轉換為我們瀏覽器可以根據這些規則顯示的Dom節點:

  • 如果type屬性持有使用標記名稱-建立一個標記,其中列出了下面列出的所有屬性props.

  • 如果我們有一個函式或類type-呼叫它並遞迴地重複一個結果。

  • 如果有什麼childrenprops-逐個重複這個過程,並將結果放置在父節點的Dom節點內。

因此,我們得到以下html(對於我們的表示例):

<table> <tr> <td>Title</td> </tr>
 ...
</table> 

重建在

在實踐中,render通常會在根元素上呼叫一次,並且進一步更新通過state.

注意:“重新”在標題中!當我們想要做的時候,真正的反應就開始了更新一頁沒有取代一切。我們怎麼能做到這一點也沒有什麼辦法。讓我們從最簡單的一個呼叫開始ReactDOM.render對於同一個節點再一次.

// Second call
ReactDOM.render(
 React.createElement(Table, { rows: rows }),
 document.getElementById(`#root`)
);

這次,上面的程式碼將與我們已經看到的不同。響應將從零開始建立所有Dom節點,並將它們放到頁面上,而響應將開始和解(或“diffing”)演算法來確定必須更新節點樹的哪些部分,並且可以保持未受影響。

那麼,它是如何工作的呢?只有幾個簡單的場景和理解他們我們的優化將會幫助我們很多。請記住,我們現在正在檢視作為響應虛擬dom中節點的表示形式的物件。

  • 設想1:type是一個字串,type在電話裡保持相同的距離props也沒有改變。
// before update
{ type: `div`, props: { className: `cn` } }

// after update
{ type: `div`, props: { className: `cn` } }

這是最簡單的例子:dom保持不變。

  • 設想2:type仍然是同一個字串,props是不同的。
// before update:
{ type: `div`, props: { className: `cn` } }

// after update:
{ type: `div`, props: { className: `cnn` } }

作為我們type仍然表示html元素響應知道如何通過標準的Dom呼叫更改其屬性,而無需從一種樹中刪除節點。

  • 設想3:type已經變了不同String或從String到元件上。
// before update:
{ type: `div`, props: { className: `cn` } }

// after update:
{ type: `span`, props: { className: `cn` } }

當響應現在看到型別不同時,它甚至不會嘗試更新我們的節點:舊元素將會被刪除(下裝) 和所有的孩子一起。因此,對於完全不同的高階別dom樹的元素替換一個元素可能非常昂貴。幸運的是,在現實世界裡很少發生這種事。

記住反應用途是很重要的===(三倍等於)比較type值,所以它們必須是相同的例項同一類或功能。

接下來的場景更有趣,因為這就是我們經常使用反應的方式。

  • 情景4:type是一個元件。
// before update:
{ type: Table, props: { rows: rows } }

// after update:
{ type: Table, props: { rows: rows } }

“可是什麼都沒變!”你也許會說,你會錯的。

注意元件的render(只有類元件已顯式定義了此方法),但與ReactDOM.render。“渲染”這個詞的確在反應世界中確實被過度使用了。

如果type是對函式或類(即正則反應元件)的引用,並且我們開始了樹的調整過程,然後反應總是試圖看元件以確保返回的值返回render沒有改變(預防副作用的一種預防)。沖洗和重複每一個元件下樹-是的,複雜的渲染也可能變得昂貴!

照顧孩子

除了上面描述的四個常見場景外,我們還需要考慮當元素有多個子時的響應行為。我們假設我們有這樣一個元素:

// ...
props: {
 children: [
 { type: `div` },
 { type: `span` },
 { type: `br` }
 ]
},
// ...

我們想把這些孩子們洗牌:

// ...
props: {
 children: [
 { type: `span` },
 { type: `div` },
 { type: `br` }
 ]
},
// ...

然後呢?

如果“diffing”,反應就會看到任何內部陣列props.children它開始比較它中的元素與它之前看到的陣列中的元素,然後依次檢視它們:索引0將與索引0、索引1和索引1等進行比較。對於每一對,反應將應用上面描述的規則集。在我們的例子中,它看到div變成了一個span所以設想3將被應用。這並不是非常有效的:想象一下,我們已經從1000行表中刪除了第一行。反應將不得不“更新”剩餘999名兒童,因為他們的內容現在不會相等,如果與先前的代表指數相比,則是相等的。

幸運的是,反應有內建來解決這個問題。如果元素具有key屬性將比較元素的值。key不是按指數來的。只要鑰匙是獨一無二的,反應就會圍繞著元素移動將它們從Dom樹中移除然後將它們放到後面(在響應中已知的過程為安裝/解除安裝).

// ...
props: {
 children: [ // Now React will look on key, not index
 { type: `div`, key: `div` },
 { type: `span`, key: `span` },
 { type: `br`, key: `bt` }
 ]
},
// ... 

當狀態發生變化時

直到現在我們才接觸到props反應哲學的一部分,但忽略了state。下面是一個簡單的“有狀態”元件:

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>)
}

所以,我們有一個counter金鑰在我們的狀態物件中。單擊按鈕時會增加其值並更改按鈕文字。但是當我們這麼做時,在一個Dom裡發生了什麼?其中哪些部分將重新計算和更新?

呼叫this.setState也會導致重新渲染,但不會導致整個頁面,但是只有一個元件本身及其孩子。父母和兄弟姐妹都是免費的。當我們擁有一棵大樹時,這很方便,我們只想重繪它的一部分。

釘住問題

我們已經準備好了小演示應用程式所以在我們去修它們之前,你可以看到野外最常見的問題。您可以檢視它的原始碼。這裡。你也需要反應開發工具所以請確保您安裝了它們為您的瀏覽器。

我們首先要看的是哪些元素和何時使虛擬dom被更新。導航到瀏覽器的dev工具中的響應皮膚,並選擇“突出更新”核取方塊:

React DevTools in Chrome with `Highlight updates` checkbox selected

在chrome中使用“突出更新”核取方塊進行響應

現在嘗試將一行新增到表中。正如您所看到的,在頁面上每個元素周圍都出現了邊框。這意味著每次新增一行時,響應都是計算和比較整個虛擬dom樹。現在嘗試在一行中打一個計數器按鈕。您可以看到虛擬dom在更改時如何更新state-只有有關因素及其子女受到影響。

對問題可能發生的地方做出反應,但告訴我們細節:尤其是更新問題意味著“diffing”元素或掛載/重新設定它們。為了找到更多的資訊,我們需要使用反應的內建探查器(注意它不會在生產模式中工作)。

?react_perf對於您的應用程式的任何url,並進入chrome瀏覽器中的“效能”選項卡。點選錄製按鈕並點選桌子周圍。新增一些行,更改一些計數器,然後點選“停止”。

React DevTools` `performance` tab

反應DevTools‘效能’選項卡

在所產生的輸出中,我們對“使用者計時”感興趣。縮放到時間線直到看到“反應樹協調”組及其孩子。這些都是我們的元件的名稱[最新情況][山]在他們旁邊。

我們的大部分業績問題都屬於這兩類。

無論是元件(以及來自它的所有分支)都是出於某些原因重新安裝在每個更新上,我們不希望它(重新安裝慢),或者我們正在執行昂貴的和解,大型分支,儘管沒有任何改變。

修理東西:安裝/安裝

現在,當我們發現了一些關於如何做出反應決定更新虛擬dom並瞭解如何檢視幕後發生的事情時,我們終於準備好修復事情了!首先,讓我們來處理坐騎/unmounts。

如果您僅僅考慮到任何元素/元件的多個子元素都表示為列陣內部。

考慮到這一點:

<div> <Message /> <Table /> <Footer /> </div> 

在我們的虛擬dom中,它將被表示為:

// ...
props: {
 children: [
 { type: Message },
 { type: Table },
 { type: Footer }
 ]
}
// ...

我們有一個簡單的Message這是一個div持有一些文字(想想您的花園品種通知)和一個巨大的Table跨越,比方說,1000+行。他們都是被包圍的孩子div所以它們被置於下面props.children在父節點上,它們不會碰巧有一個鍵。而且反應不會提醒我們通過控制檯警告來分配金鑰,因為子元素正在被傳遞給父級React.createElement作為引數列表,而不是陣列。

現在我們的使用者已經駁回了通知,Message從樹上移走。Table以及Footer剩下的都是。

// ...
props: {
 children: [
 { type: Table },
 { type: Footer }
 ]
}
// ...

反應如何看?它把它看作是一個改變形狀的兒童的陣列:children[0]持有Message現在它佔據了Table。沒有比與之相比的鍵,所以比較type因為它們都引用函式(以及異類函式)n.unmounts整體Table然後再掛載它,渲染所有的孩子:1000+行!

所以,您可以新增唯一的鍵(但是在這個特定的例子中使用鍵不是最好的選擇),或者去尋找一個更聰明的技巧:使用短路布林估計這是javascript和許多其他現代語言的特點。看:

// Using a boolean trick
<div>
 {isShown && <Message />}
 <Table /> <Footer /> </div> 

即使Message走出畫面,props.children父母div仍會持有3元素,children[0]有價值false(布林基)。記住true/false, null以及undefined是虛擬dom物件的所有允許值type財產?我們最終得出了這樣的結論:

// ...
props: {
 children: [
 false, // isShown && <Message /> evaluates to false
 { type: Table },
 { type: Footer }
 ]
}
// ...

所以,Message或者不是,我們的索引不會改變,Table當然,將仍然與Table(指元件的引用)type無論如何開始和解,但是僅僅比較虛擬dom比刪除Dom節點和從頭開始建立它們更快。.

現在讓我們來看看更進化的東西。我們知道你喜歡特設s.一個高階元件是一個函式,它將元件作為引數,做一些事情,並返回一個不同的函式:

function withName(SomeComponent) {
 // Computing name, possibly expensive... return function(props) {
 return <SomeComponent {...props} name={name} />;
 }
}

這是一個非常常見的模式,但您需要小心處理它。考慮:

class App extends React.Component() {
 render() {
 // Creates a new instance on each render const ComponentWithName = withName(SomeComponent);
 return <SomeComponentWithName />;
 }
}

我們正在建立一個父的內部的一個特殊的render方法。當我們重新渲染樹時,我們的虛擬dom看起來就像這樣:

// On first render:
{
 type: ComponentWithName,
 props: {},
}

// On second render:
{
 type: ComponentWithName, // Same name, but different instance
 props: {},
}

現在,響應將喜歡只執行一個基於上的演算法ComponentWithName但是,正如這個時候,同一個名稱引用了不同例項三重相等比較失敗,而不是和解,完全重新安裝必須發生。注意,它也會導致國家失去正如這裡所描述的。幸運的是,它很容易修復:您需要始終在render:

// Creates a new instance just once const ComponentWithName = withName(Component);

class App extends React.Component() {
 render() {
 return <ComponentWithName />;
 }
}

修復事物:更新

所以,現在我們確保不要重新安裝東西,除非必要。然而,對於位於中的樹根附近的元件的任何更改都會導致所有子樹的重新連線和協調。結構複雜,價格昂貴,而且常常可以避免。

Would be great to have a way to tell React not to look at a certain branch, as we are confident there were no changes in it.

這種方法存在,它涉及到一種稱為shouldComponentUpdate它是元件的生命週期。此方法稱為以前每個對元件的呼叫render並接收道具和狀態的新值。然後我們可以自由地將它們與當前值進行比較,並決定是否應該更新元件(返回)。truefalse那就是。如果我們返回false反應不會重新渲染所涉元件,並且不會檢視其子元素。

通常比較兩組props以及state簡單的淺層比較是足夠的:如果頂級的值不同,我們不必更新。淺比較不是javascript的特性,但卻有很多公用事業為了那個。

通過他們的幫助,我們可以編寫我們的程式碼如下:

class TableRow extends React.Component {

 // will return true if new props/state are different from old ones
 shouldComponentUpdate(nextProps, nextState) {
 const { props, state } = this;
 return !shallowequal(props, nextProps)
 && !shallowequal(state, nextState);
 }

 render() { /* ... */ }
}

但是您甚至不必自己編碼,因為響應中包含了這個特性,類呼叫 React.PureComponent。它類似於React.Component只有shouldComponentUpdate已為您實現了淺層道具/狀態比較。

聽起來好像是個沒腦子的,只是交換ComponentPureComponentextends你班的一部分定義並享受效率。不過別這麼快!考慮這些例子:

<Table
 // map returns a new instance of array so shallow comparison will fail
 rows={rows.map(/* ... */)}
 // object literal is always "different" from predecessor
 style={ { color: `red` } }
 // arrow function is a new unnamed thing in the scope, so there will always be a full diffing
 onUpdate={() => { /* ... */ }}
/>

上面的程式碼片段演示了三個最常見的程式碼反模式。儘量避開他們!

如果您注意到建立所有物件、陣列和函式之外的render定義並確保他們不會在電話之間發生變化-你是安全的。

你可以觀察到PureComponent更新演示所有桌子的位置Rows是“淨化”的。如果您在響應DevTools中開啟“突出更新”,您將注意到只有表本身和新行正在行插入中呈現,所有其他行都保持不變。

然而,如果你不能等待全力以赴在純元件上,並在您的應用程式中到處實現它們-停止自己。比較兩組props以及state不是免費的,對於大多數基本元件來說都不是值得的:要執行更多的時間。shallowCompare比在演算法。

使用這個經驗法則:純元件對複雜表單和表很好,但是它們通常會簡化一些簡單元素,比如按鈕或圖示。

原文釋出時間:04/22

原文作者: 落葉_dfg

本文來源開源中國如需轉載請緊急聯絡作者


相關文章