前言
關於react性能優化,在react 16這個版本,官方推出fiber,在框架層面優化了react性能上面的問題。由於這個太過於龐大,我們今天圍繞子自組件更新策略,從兩個及其微小的方面來談react性能優化。 其主要目的就是防止不必要的子組件渲染更新。
子組件何時更新?
首先我們看個例子,父組件如下:
import React,{Component} from 'react'; import ComponentSon from './components/ComponentSon'; import './App.css'; class App extends Component{ state = { parentMsg:'parent', sonMsg:'son' } render(){ return ( <div className="App"> <header className="App-header" onClick={()=> {this.setState({parentMsg:'parent' + Date.now()})}}> <p> {this.state.parentMsg} </p> </header> <ComponentSon sonMsg={this.state.sonMsg}/> </div> );} } export default App;
父親組件作為容器組件,管理兩個狀態,一個parentMsg用於管理自身組件,一個sonMsg用於管理子組件狀態。兩個點擊事件用於分別修改兩個狀態
子組件寫法如下:
import React,{Component} from 'react'; export default class ComponentSon extends Component{ render(){ console.log('Component rendered : ' + Date.now()); const msg = this.props.sonMsg; return ( <div> {msg}</div> ) } }
無論什么點擊哪個事件,都會觸發 ComponentSon 的渲染更新。 控制台也可以看到自組件打印出來的信息:
PureComponent rendered : 1561790880451
但是這並不是我們想要的吧?按理來說,子組件需要用到的那個props更新了,才會重新渲染更新,這個才是我們想要的。還好React 的class組件類中的shouldComponentUpdate可以解決我們的問題。
// shouldComponentUpdate import React,{Component} from 'react'; export default class ComponentSon extends Component{ shouldComponentUpdate(nextProps,nextState){ console.log('當前現有的props值為'+ this.props.sonMsg); console.log('即將傳入的props值為'+ nextProps.sonMsg); } render(){ console.log('PureComponent rendered : ' + Date.now()); const msg = this.props.sonMsg; return ( <div> {msg}</div> ) } }
注意,這個shouldComponentUpdate要返回一個boolean值的,這里我沒有返回,看看控制台怎么顯示?當我點擊修改parentMsg的元素時:
當前現有的props值為son 即將傳入的props值為son Warning: ComponentSon.shouldComponentUpdate(): Returned undefined instead of a boolean value. Make sure to return true or false.
也就是說,控制台給出了一個警告,同時shouldComponentUpdate默認返回true,即要更新子組件。因此,避免props沒有發生變化的時候更新,我們可以修改shouldComponentUpdate:
shouldComponentUpdate(nextProps,nextState){
console.log('當前現有的props值為'+ this.props.sonMsg); console.log('即將傳入的props值為'+ nextProps.sonMsg); return this.props.sonMsg !== nextProps.sonMsg }
這樣就解決了不必要的更新。
PureComponent 更新原理(class 組件)
在上述例子中,我們看到了shouldComponentUpdate在 class 組件上的積極作用,是不是每個class組件都得自己實現一個shouldComponentUpdate判斷呢?react 官方推出的PureComponent就封裝了這個,幫忙解決默認情況下子組件渲染策略的問題。
import React,{PureComponent} from 'react'; export default class ComponentSon extends PureComponent{ render(){ console.log('PureComponent rendered : ' + Date.now()); const msg = this.props.sonMsg; return ( <div> {msg}</div> ) } }
當父組件修改跟子組件無關的狀態時,再也不會觸發自組件的更新了。
用PureComponent會不會有什么缺點呢?
這里我們是傳入一個string字符串(基本數據類型)作為props傳遞給子組件。 這里我們是傳入一個object對象(引用類型)作為props傳遞給子組件。
import React,{Component} from 'react'; import ComponentSon from './components/ComponentSon'; import './App.css'; class App extends Component{ state = { parentMsg:'parent', sonMsg:{ val:'this is val of son' } } render(){ return ( <div className="App"> <header className="App-header" onClick={()=> {this.setState({parentMsg:'parent' + Date.now()})}}> <p> {this.state.parentMsg} </p> </header> <button onClick={()=> this.setState(({sonMsg}) => { sonMsg.val = 'son' + Date.now(); console.table(sonMsg); return {sonMsg} }) }>修改子組件props</button> <ComponentSon sonMsg={this.state.sonMsg}/> </div> );} } export default App;
當我們點擊button按鈕的時候,觸發了setState,修改了state,但是自組件卻沒有跟新。為什么呢?這是因為:
PureComponent 對狀態的對比是淺比較的
PureComponent 對狀態的對比是淺比較的
PureComponent 對狀態的對比是淺比較的
this.setState(({sonMsg}) => { sonMsg.val = 'son' + Date.now(); console.table(sonMsg); return {sonMsg} })
這個修改state的操作就是淺復制操作。什么意思呢?這就好比
let obj1 = { val: son } obj2 = obj1; obj2.val = son + '1234'; // obj1 的val 也同時被修改。因為他們指向同一個地方引用 obj1 === obj2;// true
也就是說,我們修改了新狀態的state,也修改了老狀態的state,兩者指向同一個地方。PureComponent 發現你傳入的props 和 此前的props 一樣的,指向同一個引用。當然不會觸發更新了。
那我們應該怎么做呢?react 和 redux中也一直強調,state是不可變的,不能直接修改當前狀態,要返回一個新的修改后狀態對象
因此,我們改成如下寫法,就可以返回一個新的對象,新對象跟其他對象肯定是不想等的,所以淺比較就會發現有變化。自組件就會有渲染更新。
this.setState(({sonMsg}) => { console.table(sonMsg); return { sonMsg:{ ...sonMsg, val:'son' + Date.now() } } })
除了上述寫法外,我們可以使用Object.assign來實現。也可以使用外部的一些js庫,比如Immutable.js等。
而此前的props是字符串字符串是不可變的(Immutable)。什么叫不可變呢?就是聲明一個字符串並賦值后,字符串再也沒法改變了(針對內存存儲的地方)。
let str = "abc"; // str 不能改變了。 str +='def' // str = abcdef
看上去str 由最開始的 ‘abc’ 變為‘abcdef’變了,實際上是沒有變。 str = ‘abc’的時候,在內存中開辟一塊空間棧存儲了abc,並將str指向這塊內存區域。 然后執行 str +='def'這段代碼的時候,又將結果abcdef存儲到新開辟的棧內存中,再將str指向這里。因此原來的內存區域的abc一直沒有變化。如果是非全局變量,或沒被引用,就會被系統垃圾回收。
forceUpdate
React.PureComponent 中的 shouldComponentUpdate() 僅作對象的淺層比較。如果對象中包含復雜的數據結構,則有可能因為無法檢查深層的差別,產生錯誤的比對結果。僅在你的 props 和 state 較為簡單時,才使用 React.PureComponent,或者在深層數據結構發生變化時調用 forceUpdate() 來確保組件被正確地更新。你也可以考慮使用 immutable 對象加速嵌套數據的比較。
此外,React.PureComponent 中的 shouldComponentUpdate() 將跳過所有子組件樹的 prop 更新。因此,請確保所有子組件也都是“純”的組件。
memo
上述我們花了很大篇幅,講的都是class組件,但是隨着hooks出來后,更多的組件都會偏向於function 寫法了。React 16.6.0推出的重要功能之一,就是React.memo。
React.memo 為高階組件。它與 React.PureComponent 非常相似,但它適用於函數組件,但不適用於 class 組件。
如果你的函數組件在給定相同 props 的情況下渲染相同的結果,那么你可以通過將其包裝在 React.memo 中調用,以此通過記憶組件渲染結果的方式來提高組件的性能表現。這意味着在這種情況下,React 將跳過渲染組件的操作並直接復用最近一次渲染的結果。
// 子組件 export default function Son(props){ console.log('MemoSon rendered : ' + Date.now()); return ( <div>{props.val}</div> ) }
上述跟class組件中沒有繼承PureComponent一樣,只要是父組件狀態更新的時候,子組件都會重新渲染。所以我們用memo來優化:
import React,{memo} from 'react'; const MemoSon = memo(function Son(props){ console.log('MemoSon rendered : ' + Date.now()); return ( <div>{props.val}</div> ) }) export default MemoSon;
默認情況下其只會對復雜對象做淺層對比,如果你想要控制對比過程,那么請將自定義的比較函數通過第二個參數傳入來實現。
function MyComponent(props) { /* 使用 props 渲染 */ } function areEqual(prevProps, nextProps) { /* 如果把 nextProps 傳入 render 方法的返回結果與 將 prevProps 傳入 render 方法的返回結果一致則返回 true, 否則返回 false */ } export default React.memo(MyComponent, areEqual);
注意點如下:
- 此方法僅作為性能優化的方式而存在。但請不要依賴它來“阻止”渲染,因為這會產生 bug。
- 與 class 組件中 shouldComponentUpdate() 方法不同的是,如果 props 相等,areEqual 會返回 true;如果 props 不相等,則返回 false。這與 shouldComponentUpdate 方法的返回值相反。
結語
本次我們討論了react組件渲染的優化方法之一。class組件對應PureComponent,function組件對應memo。也簡單講述了在使用過程中的注意點。后續開發組件的時候,或許能對性能方面有一定的提升。