天天看點

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

如今的前端,架構橫行,出去面試問到架構是常有的事。

我比較常用React, 這裡就寫了一篇 React 基礎原理的内容, 面試基本上也就問這些, 分享給大家。

React 是什麼

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

React是一個專注于建構使用者界面的 Javascript Library.

React做了什麼?
  • Virtual Dom模型
  • 生命周期管理
  • setState機制
  • Diff算法
  • React patch、事件系統
  • React的 Virtual Dom模型
virtual dom 實際上是對實際Dom的一個抽象,是一個js對象。react所有的表層操作實際上是在操作Virtual dom。

經過 Diff 算法會計算出 Virtual DOM 的差異,然後将這些差異進行實際的DOM操作更新頁面。

React 總體架構
[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化
幾點要了解的知識
  • JSX 如何生成Element
  • Element 如何生成DOM

1

JSX 如何生成Element

先看一個例子, Counter :

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

App.js 就做了一件事情,就是把 Counter 元件挂在 #root 上.

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

Counter 元件裡面定義了自己的 state, 這是個預設的 property ,還有一個 handleclick 事件和 一個 render 函數。

看到 render 這個函數裡,竟然在 JS 裡面寫了 html !

這是一種 JSX 文法。React 為了友善 View 層元件化,承載了建構 html 結構化頁面的職責。

這裡也簡單的舉個例子:

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

将 html 文法直接加入到 javascript 代碼中,再通過翻譯器轉換到純 javascript 後由浏覽器執行。

這裡調用了 React 和 createElement 方法,這個方法就是用于建立虛拟元素 Virtual Dom 的。

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

React 把真實的 DOM 樹轉換成 Javascript 對象樹,也就是 Virtual Dom。

每次資料更新後,重新計算 Virtual Dom ,并和上一次生成的 virtual dom 做對比,對發生變化的部分做批量更新。

而 React 是通過建立與更新虛拟元素 Virtual Element 來管理整個Virtual Dom 的。

虛拟元素可以了解為真實元素的對應,它的建構與更新都是在記憶體中完成的,并不會真正渲染到 dom 中去。

回到我們的計數器 counter 元件:

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

注意下 a 标簽 createElement 的傳回結果, 這裡 CreateElement 隻是做了簡單的參數修正,傳回一個 ReactElemet 執行個體對象。

Virtual element 彼此嵌套和混合,就得到了一顆 virtual dom 的樹:

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

2

Element 如何生成DOM

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

現在我們有了由 ReactElement 組成的 Virtual Dom 樹,接下來我們要怎麼我們建構好的 Virtual dom tree 渲染到真正的 DOM 裡面呢?

這時可以利用 ReactDOM.render 方法,傳入一個 reactElement 和一個 作為容器的 DOM 節點。

看進去 ReactDOM.render 的源碼,裡面有兩個比較關鍵的步驟:

第一步是 instantiateReactComponent。

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

這個函數建立一個 ReactComponent 的執行個體并傳回,也可以看到 ReactDOM.render 最後傳回的也是這個執行個體。

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

instantiateReactComponent 方法是初始化元件的入口函數,它通過判斷 node 的類型來區分不同元件的入口。

  1. 當 node 為空的時候,初始化空元件。
  2. 當 node 為對象,類型 type 字段标記為是字元串,初始化 DOM 标簽。否則初始化自定義元件。
  3. 當 node 為字元串或者數字時,初始化文本元件。
[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

雖然 Component 有多種類型,但是它們具有基本的資料結構:ReactComponent 類。

注意到這裡的 setState, 這也是重點之一。

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

建立了 Component 執行個體後,調用 component 的 mountComponent 方法,注意到這裡是會被批量 mount 的,這樣元件就開始進入渲染到 DOM 的流程了。

React生命周期
[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

React 元件基本由三個部分組成,

  1. 屬性 props
  2. 狀态 state
  3. 生命周期方法

React 元件可以接受參數props, 也有自身狀态 state。

一旦接受到的參數 props 或自身狀态 state 有所改變,React 元件就會執行相應的生命周期方法。

React 生命周期的全局圖

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

首次挂載元件時,按順序執行

  1. componentWillMount、
  2. render
  3. componentDidMount

解除安裝元件時,執行 componentDidUnmount

當元件接收到更新狀态,重新渲染元件時,執行

  1. componentWillReceiveProps
  2. shouldComponentUpdate
  3. componentWillUpdate
  4. render
  5. componentDidUpdate

更新政策

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

通過 updateComponent 更新元件,首先判讀上下文是否改變,前後元素是否一緻,如果不一緻且元件的 componentWillReceiveProps 存在,則執行。然後進行 state 的合并。

調用 shouldComponentUpdate 判斷是否需要進行元件更新,如果存在 componentWillUpdate 則執行。

後面的流程跟 mountComponent 相似,這裡就不贅述了。

setState機制

為避免篇幅過長,這部分可移步我的另一篇文章:

[第10期] 深入了解 React setState 運作機制

Diff算法

Diff算法用于計算出兩個virtual dom的差異,是React中開銷最大的地方。

傳統diff算法通過循環遞歸對比差異,算法複雜度為 O(n3)。

React diff算法制定了三條政策,将算法複雜度從 O(n3)降低到O(n)。

  • 1. UI中的DOM節點跨節點的操作特别少,可以忽略不計。
  • 2. 擁有相同類的元件會擁有相似的DOM結構。擁有不同類的元件會生成不同的DOM結構。
  • 3. 同一層級的子節點,可以根據唯一的

    ID

    來區分。

1. Tree Diff

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

對于政策一,React 對樹進行了分層比較,兩棵樹隻會對同一層次的節點進行比較。

隻會對相同層級的 DOM 節點進行比較,當發現節點已經不存在時,則該節點及其子節點會被完全删除,不會用于進一步的比較。

如果出現了 DOM 節點跨層級的移動操作。

如上圖這樣,A節點就會被直接銷毀了。

Diif 的執行情況是:create A -> create C -> create D -> delete A

2. Element Diff

  1. 當節點處于同一層級時,diff 提供了 3 種節點操作:插入、移動和删除。
  2. 對于同一層的同組子節點添加唯一 key 進行區分。
[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

通過 diff 對比後,發現新舊集合的節點都是相同的節點,是以無需進行節點删除和建立,隻需要将舊集合中節點的位置更新為新集合中節點的位置.

原了解析

幾個概念

  • 對新集合中的節點進行循環周遊,新舊集合中是否存在相同節點
  • nextIndex: 新集合中目前節點的位置
  • lastIndex: 通路過的節點在舊集合中最右的位置(最大位置)
  • If (child._mountIndex < lastIndex)

對新集合中的節點進行循環周遊,通過 key 值判斷,新舊集合中是否存在相同節點,如果存在,則進行移動操作。

在移動操作的過程中,有兩個指針需要注意,

一個是 nextIndex,表示新集合中目前節點的位置,也就是周遊新集合時目前節點的坐标。

另一個是 lastIndex,表示通路過的節點在舊集合中最右的位置,

更新流程:

1

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

( 如果新集合中目前通路的節點比 lastIndex 大,證明目前通路節點在舊集合中比上一個節點的位置靠後,則該節點不會影響其他節點的位置,即不進行移動操作。隻有目前通路節點比 lastIndex 小的時候,才需要進行移動操作。)

首先,我們開周遊新集合中的節點, 目前 lastIndex = 0, nextIndex = 0,拿到了 B.

此時在舊集合中也發現了 B,B 在舊集合中的 mountIndex 為 1 , 比目前 lastIndex 0 要大,不滿足 child._mountIndex < lastIndex,對 B 不進行移動操作,更新 lastIndex = 1, 通路過的節點在舊集合中最右的位置,也就是 B 在舊集合中的位置,nextIndex++ 進入下一步。

2

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

目前 lastIndex = 1, nextIndex = 1,拿到了 A,在舊集合中也發現了 A,A 在舊集合中的 mountIndex 為 0 , 比目前 lastIndex 1 要小,滿足 child._mountIndex < lastIndex,對 A 進行移動操作,此時 lastIndex 依然 = 1, A 的 _mountIndex 更新為 nextIndex = 1, nextIndex++, 進入下一步.

3

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

這裡,A 變成了藍色,表示對 A 進行了移動操作。

目前 lastIndex = 1, nextIndex = 2,拿到了 D,在舊集合中也發現了 D,D 在舊集合中的 mountIndex 為 3 , 比目前 lastIndex 1 要大,不滿足 child._mountIndex < lastIndex,不進行移動操作,此時 lastIndex = 3, D 的 _mountIndex 更新為 nextIndex = 2, nextIndex++, 進入下一步.

4

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

目前 lastIndex = 3, nextIndex = 3,拿到了 C,在舊集合中也發現了 C,C 在舊集合中的 mountIndex 為 2 , 比目前 lastIndex 3 要小,滿足 child._mountIndex < lastIndex,要進行移動,此時 lastIndex不變,為 3, C 的 _mountIndex 更新為 nextIndex = 3.

5

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

由于 C 已經是最後一個節點,是以 diff 操作完成.

這樣最後,要進行移動操作的隻有 A C。

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

另一種情況

剛剛說的例子是新舊集合中都是相同節點但是位置不同。

那如果新集合中有新加入的節點且舊集合存在需要删除的節點,

那 diff 又是怎麼進行的呢?比如:

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

1

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

首先,依舊,我們開周遊新集合中的節點, 目前 lastIndex = 0, nextIndex = 0,拿到了 B,此時在舊集合中也發現了 B,B 在舊集合中的 mountIndex 為 1 , 比目前 lastIndex 0 要大,不滿足 child._mountIndex < lastIndex,對 B 不進行移動操作,更新 lastIndex = 1, 通路過的節點在舊集合中最右的位置,也就是 B 在舊集合中的位置,nextIndex++ 進入下一步。

2

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

目前 lastIndex = 1, nextIndex = 1,拿到了 E,發現舊集合中并不存在 E,此時建立新節點 E,nextIndex++,進入下一步

3

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

目前 lastIndex = 1, nextIndex = 2,拿到了 C,在舊集合中也發現了 C,C 在舊集合中的 mountIndex 為 2 , 比目前 lastIndex 1 要大,不滿足 child._mountIndex < lastIndex,不進行移動,此時 lastIndex 更新為 2, nextIndex++ ,進入下一步

4

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

目前 lastIndex = 2, nextIndex = 3,拿到了 A,在舊集合中也發現了 A,A 在舊集合中的 mountIndex 為 0 , 比目前 lastIndex 2 要小,不滿足 child._mountIndex < lastIndex,進行移動,此時 lastIndex 不變, nextIndex++ ,進入下一步

5

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

當完成新集合中所有節點的差異化對比後,還需要對舊集合進行循環周遊,判斷是否勳在新集合中沒有但舊集合中存在的節點。

此時發現了 D 滿足這樣的情況,是以删除 D。

Diff 操作完成。

整個過程還是很繁瑣的, 明白過程即可。

二、性能優化

由于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} />
    }
}
           

複制

3. forceUpdate

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

其他優化政策

1. shouldComponentUpdate

使用shouldComponentUpdate鈎子,根據具體的業務狀态,減少不必要的props變化導緻的渲染。如一個不用于渲染的props導緻的update。

另外, 也要盡量避免在shouldComponentUpdate 中做一些比較複雜的操作, 比如超大資料的pick操作等。

2. 合理設計state,不需要渲染的state,盡量使用執行個體成員變量。

不需要渲染的 props,合理使用 context機制,或公共子產品(比如一個單例服務)變量來替換。

2

正确使用 diff算法

  • 不使用跨層級移動節點的操作。
  • 對于條件渲染多個節點時,盡量采用隐藏等方式切換節點,而不是替換節點。
  • 盡量避免将後面的子節點移動到前面的操作,當節點數量較多時,會産生一定的性能問題。
[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

看個具體的例子

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

這時一個 List 元件,裡面有标題,包含 ListItem 子元件的members清單,和一個按鈕,綁定了一個 onclick 事件.

然後我加了一個插件,可以顯示出各個元件的渲染情況。

現在我們來點選改變标題, 看看會發生些什麼。

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

奇怪的事情發生了,為什麼我隻改了标題, 為什麼不相關的 ListItem 元件也會重新渲染呢?

我們可以回到元件生命周期看看為什麼。

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

還記得這個元件更新的生命周期流程圖嘛,這裡的重點在于這個 shouldComponentUpdate。

隻有這個方法傳回 true 的時候,才會進行更新元件的操作。我們進步一來看看源碼。

可以看到這裡,原來如果元件沒有定義 shouldComponentUpdate 方法,也是預設認為需要更新的。

當然,我們的 ListItem 元件是沒有定義這個 shouldComponentUpdate 方法的。

然後我們使用PureComponent :

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化
[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化
[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化
[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

其原理為重新實作了 shouldComponentUpdate 生命周期方法,讓目前傳入的 props 和 state 之前做淺比較,如果傳回 false ,那麼元件就不會更新了。

這裡也放上一張官網的例圖:

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

根據渲染流程,首先會判斷shouldComponentUpdate(SCU)是否需要更新。

如果需要更新,則調用元件的render生成新的虛拟DOM,然後再與舊的虛拟DOM對比(vDOMEq)。

如果對比一緻就不更新,如果對比不同,則根據最小粒度改變去更新DOM;

如果SCU不需要更新,則直接保持不變,同時其子元素也保持不變。

相似的APi還有React.memo:

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化
回到元件

再次回到我們的元件中, 這次點選按鈕, 把第二條資料換掉:

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

奇怪的事情發生了,為什麼我隻改了第二個 listItem, 還是全部 10 個都重新渲染了呢?

原因在于 shallow compare , 淺比較。

前面說到,我們不能直接修改 this.state 的值,是以我們把

this.state.members 拷貝出來再修改第二個人的資訊。

很明顯,因為對象的比較是引用位址,顯然是不相等的。

是以 shoudComponentUpdate 方法都傳回了 false, 元件就進行了更新。

那麼我們怎麼能避免這種情況的發生呢?

其中一個方法是做深比較,但是如果對象或數組層級比較深和複制,那麼這個代價就太昂貴了。

我們就可以用到 Immutable.js 來解決這個問題,進一步提高元件的渲染性能。

Immutable Data 就是一旦被建立,就是不能再更改的資料。

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

首先,我們定義了一個 Immutable 的 List 對象,List 對應于原生 JS 的 Array,對 Immutable 對象進行修改、添加或删除操作,都會傳回一個新的 Immutable 對象,是以這裡 bbb 不等于 aaa。

但是同時為了避免深拷貝吧所有節點都複制一遍帶來的性能消耗,Immutable 使用了結構共享,即如果對象樹中一個節點發生變化,隻修改這個節點和受它影響的父節點,其他節點則進行共享。

結果也是我們預期的那樣。

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化
[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

性能分析

[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

用好火焰圖, 該優化的時候再優化。

Hooks 及其後續更新

請轉到 第7期:全面了解 React Suspense 和 Hooks

最後
[第14期] [長文預警] 深入了解React 渲染原理及性能優化 二、性能優化

關注我啦

如果你覺得内容有幫助可以關注下這個公衆号 「 前端e進階 」,一起成長!

我就知道你“在看”