效能優化問題

weixin_34120274發表於2017-09-14

真理也是一個幻覺,不過,是一個我們的生存,無法須彌或缺的幻覺。——《當尼采哭泣》

6038421-7b9146175b755907.gif
賊差的效能

1.效能問題的引出

對於上篇文章裡面所出現的react應用來說,我給todos元件的reducer函式所接受的state引數設定預設值為一個具有10000個todo項的陣列,由此渲染的時候,當我們想要對某一個todo進行反轉操作以及刪除操作的時候發現此時的效能極差,如上動圖所示,當只反轉一個todo的時候,很顯然此時差不多有了五秒的計算時間,,,。


2.為什麼會出現效能問題

雖然react已經提供了較好的渲染效能,但是還是存在可以優化效能的地方。每一次頁面的更新都是對元件的重新渲染,但是並不是將所有之前渲染的元件都全部拋棄重來,有可能只是更新,差一點的話那就是解除安裝接著更新,最好的情況就是沒有發生變化的元件不需要進行更新過程。首先我們需要知道的是,當頁面由區域性響應引起更新時,virtual dom能夠在自己的理解範圍內安全無誤的計算出對dom樹所需要做出的最少修改。

react元件的生命週期可以分為三個階段:掛載,更新,解除安裝。

掛載過程的生命週期函式有:

  • componentWillMount
  • render
  • componentDidMount

更新過程,父元件向下傳遞props或元件自身執行setState方法時觸發更新,生命週期函式有:

  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

解除安裝過程的生命週期函式有:

  • componentWillUnmount

shouldComponentUpdate以及render函式中最為重要的方法。render方法決定了元件應該渲染出什麼,而對於shouldComponentUpdate方法來說,它則決定了元件什麼時候可以不必被渲染。對於Component類所提供的shouldComponentUpdate來說,它的預設實現是始終返回true的,因此在發生更新過程的時候,元件是會預設呼叫大多數生命週期函式,包括render函式,而render函式的結果就是virtual dom。正是由於這個原因,導致有的時候react網頁的響應時的效能可能表現的不那麼理想,如果想要優化效能的話,那麼就嘗試從shouldComponentUpdate方法下手。

我們需要注意的是,對於利用無狀態函式所實現的元件來說,它的shouldComponentUpdate方法預設是繼承Component類的該方法的,即保持更新。。而對於一個無狀態函式實現的元件來說,它是修改不了shouldComponentUpdate方法的。。當然,對於這種元件的優化方法我們在後面會另作介紹。

當我們使用shouldComponentUpdate方法時,一般的做法是利用===比較符將nextProps和this.props以及nectState和this.state作比較。但是這種比較符是淺層的,對於兩個擁有者兩個相同值的不同物件來說,===是不等的。但是雖然此時兩個物件是不等的,可當它作為同一個元件的props來說,這個元件在view層卻並不用體現什麼變化。幸虧對於那些基本資料型別變數來說,===比較符不會出現這種情況。

var obj1={"a":1};
var obj2={"a":2};
obj2 === obj1;//false

3.怎麼優化效能

在上面我們也提到了想要優化元件效能的話,那就是對每個元件都實現其各自的shouldComponentUpdate方法。此時有兩個問題:

  • 1.對於react-redux應用來說,我們一般將元件分為容器元件以及檢視元件,對於容器元件來說,react-redux替我們封裝了邏輯她完全由connect方法主導產生,那麼我們又該如何定義容器元件的shouldComponentUpdate呢?
  • 2.可是對於無狀態函式實現的元件來說,又得怎麼為其定義呢?
  • 3.在上面也同時提到了,當我們實現shouldComponentUpdate的時候,常規做法就是利用===比較前後props以及前後state。可是當具有相同值的不同物件作為一個元件的props的話,那麼這個元件單單利用===實現shouldComponentUpdate的話,那麼這個元件將會進行重渲染。那麼又該如何避免這種情況呢?
//利用===邏輯實現shouldComponentUpdate將無濟於事
<Song  data={"url": "http://music.163.com/#/m/song?id=28068836&userid=67923532"} />

那麼對於上面提到的三個問題又有什麼解決辦法呢?

1.對於由react-redux提供的connect方法來說,由它實現的容器元件會有一個與父類不同的shouldComponentUpdate實現,問題是對於容器元件的shouldComponentUpdate函式來說判斷了哪些內容。我們知道對於容器元件來說,他自己可能接受props,容器元件不負責show,容器元件對應的子檢視元件才負責被使用者響應以及show。那麼問題來了,這個容器元件的shouldComponentUpdate比較了那些內容?答案是即比較了自身的props也比較了自身的state——這些state會被mapStateToProps函式處理為作為檢視元件的props。因此react-redux替容器元件實現的shouldComponentUpdate實現了自身在什麼時候得重新渲染。需要如果我們的檢視元件如果是無狀態函式元件的話,那麼connect之後還是無狀態函式元件。那麼問題又來了,我們的檢視元件的shouldComponentUpdate又如何實現?如果父元件阻斷了重渲染的話,那麼它的子元件還會進行重渲染嗎?已知這個子元件和virtual dom樹的唯一聯絡就是通過其父元件,目前為止,我的猜測是既然他不會被無故牽連那麼按理說不會平白無故被進行重渲染。????

2.對於無狀態函式元件來說,如果我們希望給定義一個shouldComponentUpdate函式的話,那麼按理來說是辦不到的。畢竟這個元件都不是以class形式定義的,那麼問題來了,該如何解決?答案是通過react-redux的connect,connect?對,我們可以利用connect方法為無狀態函式元件包裝一下,由於它不同於容器元件的子元件——檢視元件,所以在connect的時候,不需要傳入mapStateToProps以及mapDispatchToProps這兩個引數。像下面這樣的形式:

export default connect()(無狀態函式元件)

需要注意的是,此時我們的無狀態函式元件依舊是一個無狀態元件,因此它是沒有自己定義的shouldComponentUpdate?

3.在這種情況下,我們應該給元件傳入一個只被建立一次的變數。當然,事實上,這種問題遠比上面提到的那各個更加難以解決,但是要注意的就是別犯下面的錯誤:

<Funk style={{"color": "#233"}} />
<Rock song={() => {console.log("NARUTO")}} />

4.todoApp的具體優化

綜上所訴,我們可以在我們的todoApp裡尋找優化路徑,比如像下面這樣所列舉出來的:

//todos/view/addTodo.js line 55
style = {{"width": "600px"}}

這樣的話,對於react的shouldComponentUpdate來說,利用淺層比較符的話它識別不了style prop 並沒有對造成渲染view產生什麼不同的影響。因此會重複渲染,改進方式也很簡單:

const inputStyle = {"width": "600px"};


style={inputStyle}

像上面這樣的形式在我們的檢視元件裡面還存在很多,像下面這樣的:

//todos/view/listItem.js 
<li style={{.....}} />
<button style={{.....}} />
<span style={{.....}} />

上面所列舉出來的效能問題和上面的情況是一樣的,所以優化方式也沒有什麼差別,問題是對於span的style props來說,它的某個style的屬性是必須依賴元件的finished props的,因此,暫時想不出方法對他進行進一步優化。

還有就是對於一個問題就是對於無狀態元件的優化問題,上面也提到了那就是利用react-redux提供的connect方法:

export default connect()(ListItem)

對於todoList檔案,存在著一個較難優化的點:

//line 33, line 34
handleReverse={() => {handleReverse(todo.id)}}
handleDelete={() => {handleDelete(todo.id)}}

在這裡我們給ListItem傳入了handleReverse以及handleDelete這兩個props,但是這兩個props的值在每次建立ListItem元件的時候都會建立一次,所以我們需要給他傳入一個只建立一次的函式,但是也得順利完成這裡的邏輯,那麼可以像下面這樣做:

<ListItem 
    key={todo.id}
    id={todo.id}
    value={todo.text} 
    handleReverse={handleReverse}
    handleDelete={handleDelete}
    finished={todo.finished}
/>

相應的在listItem檔案增加下面這些邏輯

const mapStateToProps = (state, ownProps) => ({
    "value": ownProps.value,
    "finished": ownProps.finished
})

const mapDispatchToProps = (dispatch, ownProps) => ({
    "onReverse": () => {ownProps.handleReverse(ownProps.id)},
    "onDelete": () => {ownProps.handleDelete(ownProps.id)}
})

export default connect(mapStateToProps, mapDispatchToProps)(ListItem)

此時,最基本的效能問題已經差不多解決,下面可以看看效果:

6038421-dcd0e69cfcc164cb.gif
改善了許多

專案地址

END

相關文章