react基本原理及性能優化

1、從React原理談起

react是什麼?

clipboard.png

react是用於構建用戶界面的JS框架。所以react只負責解決view層的渲染。前端

react作了什麼?

  • Virtual Dom模型
  • 生命週期管理
  • setState機制
  • diff算法
  • React patch、事件系統
  • react的 Virtual Dom模型
virtual dom 其實是對實際Dom的一個抽象,是一個js對象。react全部的表層操做其實是在操做virtual dom。

通過diff算法會計算出virtual dom的差別,而後將這些差別進行實際的dom操做更新頁面。react

react的生命週期

clipboard.png

setState機制

理想狀況:

setState是「異步」的,調用setState只會提交一次state修改到隊列中,不會直接修改this.state。算法

等到知足必定條件時,react會合並隊列中的全部修改,觸發一次update流程,更新this.state。性能優化

所以setState機制減小了update流程的觸發次數,從而提升了性能。框架

因爲setState會觸發update過程,所以在update過程當中必經的生命週期中調用setState會存在循環調用的風險。dom

另外用於監聽state更新完成,可使用setState方法的第二個參數,回調函數。在這個回調中讀取this.state就是已經批量更新後的結果。異步

特殊狀況:

在實際開發中,setState的表現有時會不一樣於理想狀況。主要是如下兩種。函數

在mount流程中調用setState。
在setTimeout/Promise回調中調用setState。
在第一種狀況下,不會進入update流程,隊列在mount時合併修改並render。性能

在第二種狀況下,setState將不會進行隊列的批更新,而是直接觸發一次update流程。學習

這是因爲setState的兩種更新機制致使的,只有在批量更新模式中,纔會是「異步」的。

diff算法

diff算法用於計算出兩個virtual dom的差別,是react中開銷最大的地方。

傳統diff算法經過循環遞歸對比差別,算法複雜度爲O(n3)。

react diff算法制定了三條策略,將算法複雜度從 O(n3)下降到O(n)。

  • WebUI中DOM節點跨節點的操做特別少,能夠忽略不計。
  • 擁有相同類的組件會擁有類似的DOM結構。擁有不一樣類的組件會生成不一樣的DOM結構。
  • 同一層級的子節點,能夠根據惟一的ID來區分。

針對這三個策略,react diff實施的具體策略是:

  1. diff對樹進行分層比較,只對比兩棵樹同級別的節點。跨層級移動節點,將會致使節點刪除,從新插入,沒法複用。
  2. diff對組件進行類比較,類相同的遞歸diff子節點,不一樣的直接銷燬重建。diff對同一層級的子節點進行處理時,會根據key進行簡要的複用。兩棵樹中存在相同key的節點時,只會移動節點。

另外,在對比同一層級的子節點時:

diff算法會以新樹的第一個子節點做爲起點遍歷新樹,尋找舊樹中與之相同的節點。

若是節點存在,則移動位置。若是不存在,則新建一個節點。

在這過程當中,維護了一個字段lastIndex,這個字段表示已遍歷的全部新樹子節點在舊樹中最大的index。
在移動操做時,只有舊index小於lastIndex的纔會移動。

這個順序優化方案其實是基於一個假設,大部分的列表操做應該是保證列表基本有序的。
能夠推倒倒序的狀況下,子節點列表diff的算法複雜度爲O(n2)

clipboard.png

clipboard.png

clipboard.png

2、性能優化方案

因爲react中性能主要耗費在於update階段的diff算法,所以性能優化也主要針對diff算法。

1.減小diff算法觸發次數

減小diff算法觸發次數實際上就是減小update流程的次數。
正常進入update流程有三種方式:

1.setState

setState機制在正常運行時,因爲批更新策略,已經下降了update過程的觸發次數。
所以,setState優化主要在於非批更新階段中(timeout/Promise回調),減小setState的觸發次數。
常見的業務場景即處理接口回調時,不管數據處理多麼複雜,保證最後只調用一次setState。

2.父組件render

父組件的render必然會觸發子組件進入update階段(不管props是否更新)。此時最經常使用的優化方案即爲shouldComponentUpdate方法。
最多見的方式爲進行this.props和this.state的淺比較來判斷組件是否須要更新。或者直接使用PureComponent,原理一致。
須要注意的是,父組件的render函數若是寫的不規範,將會致使上述的策略失效。

// Bad case
// 每次父組件觸發render 將致使傳入的handleClick參數都是一個全新的匿名函數引用。
// 若是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
// 在構造函數中綁定函數,給變量賦值
// render中用到的常量提取成模塊變量或靜態成員
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} />
    }
}

forceUpdate

其中forceUpdate方法調用後將會直接進入componentWillUpdate階段,沒法攔截,所以在實際項目中應該棄用。

其餘優化策略

  • shouldComponentUpdate

使用shouldComponentUpdate鉤子,根據具體的業務狀態,減小沒必要要的props變化致使的渲染。如一個不用於渲染的props致使的update。
另外, 也要儘可能避免在shouldComponentUpdate 中作一些比較複雜的操做, 好比超大數據的pick操做等。

  • 合理設計state,不須要渲染的state,儘可能使用實例成員變量。

不須要渲染的props,合理使用context機制,或公共模塊(好比一個單例服務)變量來替換。

2.正確使用diff算法

  • 不使用跨層級移動節點的操做。
  • 對於條件渲染多個節點時,儘可能採用隱藏等方式切換節點,而不是替換節點。
  • 儘可能避免將後面的子節點移動到前面的操做,當節點數量較多時,會產生必定的性能問題。

未完待續

最後

以爲內容有幫助能夠關注下個人公衆號 「 前端e進階 」,掌握最新動態,一塊兒學習成長!

clipboard.png

相關文章
相關標籤/搜索