4、React元件之效能優化

keywords發表於2018-03-02

React元件的效能優化

高德納: “我們應該忘記忽略很小的效能優化,可以說97%的情況下,過早的優化是萬惡之源,
而我們應該關心對效能影響最關鍵的另外3%的程式碼。”

  • 不要將效能優化的精力浪費在對整體效能提高不大的程式碼上,而對效能有關鍵影響的部分,

優化並不嫌早。因為,對效能影響最關鍵的部分,往往涉及解決方案核心,決定整體的架構,
將來要改變的時候牽扯更大。

1. 單個React元件的效能優化

  • React利用Virtual DOM來提升渲染效能,雖然每一次頁面更新都是最元件的從新渲染,

但是並不是將之前的渲染內容全部拋棄重來,藉助Virtual DOM,React能夠計算出對DOM
樹的最少修改,這就是React預設情況下渲染都很迅速的祕訣;

  • 不過,雖然Virtual DOM能夠將每次DOM操作量減少到最小,但,計算和比較Virtual DOM依然是一個複雜的過程;
  • 當然,如果能夠在開始計算Virtual DOM之前就判斷渲染的結果不會有變化,那麼就可以不進行Virtual DOM計算和比較,速度就會更快。

2.shouldComponentUpdate的預設實現方式

  • 既然可以對元件在開始計算Virtual DOM之前判斷渲染結果不會有變化時,阻止渲染的進行,
    從而提升效能,那麼我們自然想到使用shouldComponentUpdate(nextProp,nextState)
  • shouldComponentUpdate函式在render函式之前呼叫,決定“什麼時候不需要從新渲染”;
  • 即返回一個布林值,決定更新是否進行下去,預設返回true,若返回false則中斷更新;
shouldComponentUpdate(nextProp,nextState){

    return (nextProp.completed !== this.props.completed) ||
        (nextProp.text !== this.props.text)
}
  • 其中nextProps為此次更新傳入的props,對於這個元件,影響渲染內容的prop只有completed和text,
    只要確保這兩個prop沒有變化,shouldComponentUpdate就可以返回false阻止沒必要的更新

    但是,上述的比較只是‘淺層比較’,如果型別是基本型別,只要值相同,那麼“淺層比較”
    也會認為二者相同:

    1. 那,如果prop的型別是複雜的物件怎麼辦?
    • 對於複雜物件,‘淺層比較’的方式只看這兩個prop是不是同一個物件的引用,如果不是,哪怕
      物件中的內容完全一樣也會認為是不同的兩個prop。
    • 那麼使用“深層比較”:但對物件的結構是無法預知的,如果遞迴對每個欄位都進行“深層比較”,
      不光會讓程式碼更加複雜,也可能會造成效能問題。
  • 所以,要想判斷前後的物件型別的prop是相同的,就必須要保證prop是指向同一個JavaScript物件:

    <Foo styleProp = {{color: "red"}}>
    • 要避免使用上面的傳入方式,應為每次渲染都會重新建立{color: “red”}物件,引用地址每次都不同,將導致每次的styleProp都不同。

      const footStyle = {color: "red"};//確保這個初始化只執行一次,不要放在render函式中
      
      <Foo styleProp = {footStyle}>
  • 使用‘單例模式’確保傳入的styleProp指向同一個物件
  • 如果是函式呢?

    <Foo onToggle={() => onToggleTodo(item.id)}/>
  • 應該避免使用上面的函式傳遞模式,因為這裡賦值的是一個匿名函式,而且是在賦值的時候產生的,也就是說
    每次渲染都會產生一個新的函式,這就是問題所在。
  1. 如果要傳遞的prop很多呢?

    • 恩~~用React-Redux的話,有對shouldComponentUpdate的預設實現。

3. 對多個React元件的效能優化

  • 當一個React元件被裝載、更新和解除安裝時,元件的一序列生命週期函式會被呼叫。不過,這些生命週期函式是針對一個
    特定的React元件函式,在一個應用中,從上而下有很多React元件組合起來,它們之間的渲染過程要更加複雜。
  • 同樣一個元件的渲染過程也要考慮三個過程:裝載階段、更新階段、解除安裝階段
  • 對於裝載階段,元件無論如何都要徹底渲染一次,從這個React元件往下的所有子元件,都要經歷一遍React元件的裝載生命
    週期,所以並沒有多少優化的事情可做。
  • 對於解除安裝階段,只有一個生命週期函式componentWillUnmount,這個函式只是清理componentDidMount新增的事件處理監聽等收尾工作,
    所以,也沒有什麼可優化的空間;

4. React更新階段的調和(Reconciliation)過程

  • 在元件更新過程,會構建更新Virtual DOM,並將其與之前的Virtual DOM進行比較,從而找出不同之處,使用最少的DOM操作進行更新
  • 調和過程:即React更新中對Virtual DOM找不同的過程,通常對比兩個N個節點的樹形結構的演算法,時間複雜度是O(n*3),如果直接
    使用預設對比,節點過多的話,需要操作的數量太多,而React不可能採用這種演算法;
  • React實際採用的演算法時間複雜度是O(N)(時間複雜度只是對一個演算法最好和最差情況下需要的指令運算元量級的估量)
  • React的Reconciliation演算法並不複雜,首先檢查兩個樹形的根節點的型別是否相同,根據相同或者不同有不同的處理方式:
  1. 節點型別不同的情況

    • 如果樹形節點的型別不相同,那就意味著改動很大,直接認為原來的那個樹形結構已經沒用,可以扔掉,需要從新構建DOM樹,
      原有的樹形上的React元件便會經歷“解除安裝”的生命週期;
    • 也就是說,對於Virtual DOM樹這是一個“更新”過程,但是卻可能引發這個樹結構上某些元件的“裝載”和“解除安裝”過程
      如:
      更新前
      <div>
       <Todos />
      </div>

    我們想要更新成這樣:

      <span>
          <Todos />
      </span>
>1. 那麼在作比較的時候,一看根節點原來是div,新的是span,型別就不一樣了,那麼這個演算法就廢棄之前的div包括裡面的所有子節點,
  從新構建一個span節點和子節點;
  
>2. 很明顯因為根節點不同就將所有的子節點從新構建,這很浪費,但是為了避免O(N*3)的時間複雜度,React這能選擇這種比較簡單、快捷的方法;
  
>3. 所以,作為開發者,我們一定要避免上面的浪費的情景出現
  1. 節點型別相同的情況

    • 如果兩個節點型別相同時,對於DOM元素,React會保留節點對應的DOM元素,只對其節點的屬性和內容做對比,然後只修改更新的部分;
    • 節點型別相同時,對於React元件型別,React做得是根據新節點的props去更新節點的元件例項,引發元件的更新過程;
    • 在處理完根節點對比後,React的演算法會對根節點的每一個子節點重複一樣的操作
  2. 多個相同子元件的情況

    • 如果最初元件狀態為:
    <ul>
        <TodoItem text = "First" />
        <TodoItem text = "Second" />
    
    </ul>
    • 更新後為:
    <ul>
        <TodoItem text = "First" />
        <TodoItem text = "Second" />
        <TodoItem text = "Third" />
    </ul>
    • 那麼React會建立一個新的TodoItem元件例項,而前兩個則進行正常的更新過程但是,如果更新後為:
    <ul>
        <TodoItem text = "Zero" />
        <TodoItem text = "First" />
        <TodoItem text = "Second" />
    
    </ul>
    • (這將暴露一個問題)理想處理方式是,建立一個新的TodoItem元件例項放在第一位,後兩個進入自然更新過程
      但是要讓react按照這種方式,就必須找兩個子元件的不同之處,而現有計算兩個序列差異的演算法時間是O(N*2),顯然則
      不適合對效能要求很高的場景,所以React選擇了一個看起來很傻的辦法,即挨個比較每個子元件;
    • React首先認為把text為First的元件的text改為Zero,Second的改為First,最後建立一個text為Second的元件,這樣便會破原有的兩個元件完成一個更新過程,並建立一個text為Second的新元件
    • 這顯然是一個浪費,React也意到,並提供了方克服,不過需要開發人員提供一點幫助,這就是key
  3. Key的使用
  • key屬性可以明確的告訴React每個元件的唯一標識

    • 如果最初元件狀態為:
    <ul>
        <TodoItem key={1} text = "First" />
        <TodoItem key={2} text = "Second" />
    
    </ul>
  • 更新後為:

    <ul>
        <TodoItem key={0} text = "Zero" />
        <TodoItem key={1} text = "First" />
        <TodoItem key={2} text = "Second" />
    </ul>

    因為有唯一標識key,React可以根據key值,知道現在的第二和第三個元件就是之前的第一和第二個,便用原來的props啟動更新過程,
    這樣shouldComponentUpdate就會發生作用,避免無謂的更新;

  • 注意:因為作為元件的唯一標識,所以key必須唯一,且不可變
  • 下面的程式碼是錯誤的例子:
<ul>
    todos.map((item,index) => {
            <TodoItem
                key={index}
                text={item.text}
            />
        })
</ul>

使用陣列下標作為key值,看起來唯一,但不穩定,因為隨著todos陣列值的不同,同樣一個元件例項在不同的更新過程中陣列的下標完全可能不同,
把下標當做可以就會讓React亂套,記住key不僅要唯一還要確保穩定不可變

需要注意:雖然key是一個prop,但是接受key的元件不能讀取key的值,因為key和ref是React保留的兩個特殊prop,並沒有預期讓組將直接訪問。

相關文章