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,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的作用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章