1. 程式人生 > >React總結篇之五_React元件的效能優化

React總結篇之五_React元件的效能優化

一、單個React元件的效能優化
React利用Virtual DOM來提高渲染效能,雖然這能將每次DOM操作量減少到最小,計算和比較Virtual DOM依然是一個複雜的計算過程。如果能夠在計算Virtual DOM之前就能判斷渲染結果不會有變化,那樣可以乾脆不要進行Virtual DOM計算和比較,速度就會更快。

  1. 發現浪費的渲染時間
    在Chrome瀏覽器中安裝React Perf擴充套件,步驟省略(屬於操作部分)

  2. 效能優化的時機
    “我們應該忘記忽略很小的效能優化,可以說97%的情況下,過早的優化是萬惡之源,而我們應該關心對效能影響最關鍵的那另外3%的程式碼” --高德納
    對於合併多個字串,怎樣合併,使用什麼方法合併不大可能對整個應用造成關鍵的效能影響,這就是高納德所說的97%的情況,而選擇用什麼樣的方式去定義元件的介面,如何定義state到prop的轉變,使用什麼樣的演算法來比對Virtual DOM,這些決定對效能和架構的影響是巨大的,就是那關鍵的3%。

  3. React-Redux的shouldComponentUpdate的實現
    使用React-Redux,一個典型的React元件程式碼檔案最後一個語句程式碼是這樣的:
    export default connect(mapStateToProps)(mapDispatchToProps)(Foo)
    以上,connect過程中實際上產生了一個無名的React元件類,這個類定製了shouldComponentUpdate的實現,實現邏輯是比對這次傳遞給內層傻瓜元件的props和上次的props,如果相同那就沒必要重新渲染了,可以返回false,否則就要返回true。
    但是,我們需要了解一下shouldComponentUpdate的實現方式,shouldComponentUpdate在比對prop和上次渲染所用的prop方面,依然用的是儘量簡單的方法,做的是所謂的“淺層比較”。簡單來說就是用JavaScript的===的操作符來比較,如果prop的型別是字串或者數字,只要值相同,那麼“淺層比較”也會認為二者相同,但是,如果prop的型別是複雜物件,那麼“淺層比較”的方式只看這兩個prop是不是同一個物件的引用,如果不是,哪怕這兩個物件中的內容完全一樣,也會被認為是兩個不同的prop。

    比如,在JSX中使用元件Foo的時候給名為style的prop賦值,程式碼如下:
    <Foo style={{color:"red"}} />
    像上面這樣的使用方法,Foo元件利用React-Redux提供的shouldComponentUpdate函式實現,每一次渲染都會認為style這個prop發生了變化,因為每次都會產生一個新的物件給style,而在“淺層比較”中,只比較第一層,不會去比較物件裡面是不是相等。那為什麼不用深層比較呢?因為一個物件到底有多少層無法預料,如果遞迴對每個欄位都進行“深層比較”,不光程式碼更復雜,也可能會造成效能問題。
    上面的例子應該改成下面這樣:
    const fooStyle = {color:"red"} //確保這個初始化只執行一次,不要放在render中
    <Foo style={fooStyle} />
    同樣的情況也存在與函式型別的prop,React-Redux無從知道兩個不同的函式是不是做著同樣的事,要想讓它認為兩個prop是相同的,就必須讓這兩個prop指向同樣一個函式,如果每次傳給prop的都是一個新建立的函式,那肯定就沒法讓prop指向同一個函數了。
    看TodoList傳遞給TodoItem的onToggle和onRemove,在JSX中程式碼如下:
    onToggle = {()=>onToggleTodo(item.id)}
    onRemove = {()=>onRemoveTodo(item.id)}
    這裡賦值給onClick的是一個匿名的函式,而且是在賦值的時候產生的。也就是說,每次渲染一個TodoItem的時候,都會產生一個新的函式,這就是問題所在。辦法就是不要讓TodoList每次都傳遞新的函式給TodoItem。有兩種解決方式。
    (1)第一種方式,TodoList保證傳遞給TodoItem的onToggle永遠只能指向同一個函式物件,這是為了應對TodoItem的shouldComponentUpdate的檢查,但是因為TodoItem可能有多個例項,所以這個函式要用某種方法區分什麼TodoItem回撥這個函式,區分的辦法只能通過函式引數。
    在TodoList元件中,mapDispatchToProps產生的prop中onToggleTodo接受TodoItem的id作為引數,恰好勝任這個工作,所以,可以在JSX中程式碼改為下面這樣:
    <TodoItem
    key=em.id
    id=em.id
    text=em.text
    completed=em.completed
    onToggle={onToggleTodo}
    onRemove={onRemoveTodo}
    />
    注意,除了onToggle和onRemove的值變了,還增加了一個新的prop名為id,這是讓每個TodoItem知道自己的id,在回撥onToggle和onRemove時可以區分不同的Todo-Item例項。
    TodoList的程式碼簡化了,但是TodoItem元件也要做對應改變,對應TodoItem元件的mapDispatchToProps函式程式碼如下:
    const mapDispatchToProps = (dispatch,ownProps) =>({
    onToggleItem : () => ownProps.onToggle(ownProps.id)
    });
    mapDispatchToProps這個函式有兩個引數dispatch和ownProps,也就是父元件渲染當前元件時傳遞過來的props,通過訪問ownProps.id就能夠得到父元件傳遞過來的名為id的prop值。
    上面的mapDispatchToProps這個函式給TodoItem元件增加了名為onToggleItem的prop,呼叫onToggle,傳遞當前例項的id作為引數,在TodoItem的JSX中就應該使用onToggleItem,而不是直接使用TodoList提供的onToggle。
    (2)第二種方式,乾脆讓TodoList不要給TodoItem傳遞任何函式型別prop,點選事件完全由TodoItem元件自己搞定。
    在TodoList元件的JSX中,渲染TodoItem元件的程式碼如下:
    <TodoItem
    key = em.id
    id = em.id
    text = em.text
    completed = em.completed
    />
    可以看到不需要onToggle和onRemove這些函式型別的prop,但依然有名為id的prop。
    在TodoItem元件中,需要自己通過react-redux派發action,需要改變的程式碼如下:
    const mapDispatchToprops = (dispatch,ownProps) = >{
    const id = ownProps.id;
    return {
    onToggle : () => dispatch(toggleTodo(id)),
    onRemove : () => dispatch(removeTodo(id))
    }
    };
    對比這兩種方式,看一看到無論如何TodoItem都要使用react-redux,都需要定義產生定製prop的mapDispatchToProps,都需要TodoList傳入一個id,區別只在於actions是由父組價匯入還是元件自己匯入。
    相比而言,沒有多大必要讓action在TodoList匯入然後傳遞一個函式給TodoItem,第二種讓TodoItem處理自己的一切事物,更符合高內聚的要求。

二、多個React元件的效能優化
和單個元件的生命週期一樣,React元件也要考慮3個階段:裝載階段、更新階段、解除安裝階段。其中,裝載階段基本沒什麼可以優化的空間,因為這部分工作沒有什麼可以省略的。而解除安裝階段,只有一個生命週期函式componentWillUnmount,這個函式做的事情只是清理componentDidMount新增的事件處理監聽等收尾工作,做的事情要比裝載過程少很多,所以也沒什麼可以優化的空間。所以值得關注的過程,只剩下更新過程。

  1. React的調和過程
    React在更新階段,很巧妙的對比原有的Virtual DOM和新生成的Virtual DOM(存在於記憶體中),找出兩者的不同,根據不同修改DOM樹,這樣只需做最小的必要改動。
    React在更新中找不同的過程,就叫做調和(Reconciliation)。
    React實際採用的演算法的時間複雜度是O(N)。React的Reconciliation演算法並不複雜,當React要對比兩個Virtual DOM的樹形結構的時候,從根節點開始遞迴往下對比,在樹形結構上,每個節點都可以看做這個節點以下子樹部分的根節點,所以其實這個對比演算法可以從Virtual DOM上的任何一個節點開始執行。
    React首先檢查兩個根節點的型別是否相同,根據相同或者不同有不同處理方式。
    (1)節點型別不同的情況
    這時可以直接認為原來的樹形結構已經沒用,需要重新構建新的DOM樹,原有樹形上的React元件會經歷“解除安裝”的生命週期。這時,componentWillUnmount的方法會被呼叫,取而代之的元件則會經歷裝載過程的生命週期,元件的componentWillMount、render和componentDidMount方法會被依次呼叫。
    (2)節點型別相同的情況
    這時React就會認為原來的根節點只需要更新,不必將其解除安裝,也不會引發根節點的重新裝載。
    這時,有必要區分一下節點的型別,節點的型別可以分為兩類:一類是DOM元素型別,對應的就是HTML直接支援的元素型別,比如<div />,<span />和<p />;另一類是React元件,也就是利用React庫定製的型別。
    • 對於DOM元素型別,React會保留節點對應的DOM元素,只對樹形結構根節點上的屬性和內容做一下對比,然後只更新修改的部分。
    • 對於React元件型別,React會根據新節點的props去更新原來根節點的props例項,引發這個元件例項的更新過程,也就是按照順序引發下列函式:
      shouldComponentUpdate
      componentWillReceiveProps
      componentWillUpdate
      render
      componentDidUpdate
      在處理完根節點的對比之後,React的演算法會對根節點的每個子節點重複一樣的動作,這時候每個子節點就會成為它所覆蓋部分的根節點,處理方式和它的父節點完全一樣。
      (3)多個子元件的情況
      當一個元件包含多個子元件,React的處理方式也非常的簡單直接。
      React總結篇之五_React元件的效能優化
      React發現多了一個TodoItem,會建立一個新的TodoItem元件例項,這個TodoItem元件例項需要經歷裝載過程,對於前兩個TodoItem例項,React會引發它們的更新過程。
      上面的例子是TodoItem序列後面增加了一個新的TodoItem例項,接下來在TodoItem序列前面增加一個TodoItem例項,程式碼如下:
      React總結篇之五_React元件的效能優化
      像上面新的TodoItem例項插入在第一位的例子中,React會首先認為把text為First的TodoItem元件例項的text改成了Zero,text為Second的TodoItem元件例項的text改成了First,在後面多出了一個TodoItem元件例項,text內容為Second。這樣操作的後果就是,現存的兩個TodoItem例項的text屬性被改變了,強迫它們完成了一個更新過程。React提供了方法來克服這種浪費,但需要開發人員在寫程式碼的時候提供一點幫助,這就是key的作用。