Deep In React (一) 高效能React元件
在React內部,存在著初始化渲染和更新渲染的概念。
初始化渲染會在元件第一次掛載時,渲染所有的節點

當我們希望改變某個子節點時

我們所期望React幫我們實現的渲染行為是這樣的

我們希望當我們的props向下傳遞時,只有對應需要更新的節點進行更新並重新渲染,其餘節點不需要更新和重新渲染。
但是事實上,在預設的情況下,結果卻是這樣的

所有的元件樹都被重新渲染,因為對於React而言,只要有props或者state發生了改變,我的元件就要重新渲染,所以除了綠色的節點,所有的黃色節點也被渲染了。
例子:
const Foo = ({foo}) => { console.log('Foo is rendering!'); return (<div>Foo{foo}</div>); } const Bar = ({bar}) => { console.log('Bar is rendering!'); return (<div>Bar{bar}</div>); } const FooBarGroup = ({foo, bar}) => { console.log('FooBar is rendering!'); return ( <div> <Foo foo={foo} /> <Bar bar={bar} /> </div> ) } class App extends React.Component { constructor(props) { super(props) this.state = { foo: 0, bar: 0 }; this.handleFooClick = this.handleFooClick.bind(this); this.handleBarClick = this.handleBarClick.bind(this); } handleFooClick (e){ e.preventDefault(); const newFoo = this.state.foo + 1; this.setState({foo: newFoo}); } handleBarClick(e){ e.preventDefault(); const newBar = this.state.bar + 1; this.setState({bar: newBar}); } render() { const {foo, bar} = this.state; return ( <div className="App"> <button onClick={this.handleFooClick}>Foo</button> <button onClick={this.handleBarClick}>Bar</button> <FooBarGroup foo={foo} bar={bar} /> </div> ); } } 複製程式碼
ofollow,noindex">demo on stackblitz
當我們點選Foo按鈕的時候,因為只有傳入Foo元件和FooBarGroup元件的foo這個props更新了,我們希望上只有Foo元件和FooBarGroup元件會被再次渲染。但是開啟console你會發現,console中會出現Bar元件渲染時列印的log。證明Bar元件也被重新渲染了。
shouldComponentUpdate
工作原理
避免冗餘渲染是一個常見的React效能優化方向。造成冗餘渲染的原因是在預設情況下,shouldComponentUpdate()這個生命週期函式總是返回true( source code )意味著所有的元件在預設的情況下都會在元件樹更新時去觸發render方法。
React官方對於shouldComponentUpdate的工作與React元件樹更新的機制有一個還不錯的解釋。React元件的更新決策可以分為兩步,通過shouldComponetUpdate來確認是否需要重新渲染,React vDOM diff來確定是否需要進行DOM更新操作。 shouldComponentUpdate In Action

在上圖中,C2節點不會重新觸發render函式因為shouldComponentUpdate在C2節點返回了false,更新請求在此處被截斷,相應的C2節點下的C4、C5節點也就不會觸發render函式。
對於C3節點,由於shouldComponentUpdate返回了true,所以需要進行進一步的Vitural DOM的diff(以下簡稱vDOM diff,該diff演算法由react提供,不在這細講)。
而父元件的vDOM diff其實是對於子元件遍歷進行以上過程。同上,C3的子元件C6由於shouldComponentUpdate返回了true,所以需要進行下一步vDOM diff,diff後發現需要更新,所以會重新觸發渲染。而C7節點由於shouldComponentUpdate返回了false,所以便不再進行進一步的vDOM diff。而C8節點在vDOM diff後發現vDOM相等,最後其實也不會更新。
如何使用
上面提到了,預設情況下,shouldComponentUpdate這個方法總是會返回True。如果我們需要去顯式的去決定我們的元件是否需要更新,那就意味著我們可以去顯式呼叫這個生命週期函式。
class Foo extends React.Component { shouldComponentUpdate(nextProps) { return this.props.foo !== nextProps.foo; } render(){ console.log('Foo is rendering!'); return ( <div>{ this.props.foo }</div> ) } } const Bar = ({bar}) => { console.log('Bar is rendering!'); return (<div>{bar}</div>); } const FooBarGroup = ({foo, bar}) => { console.log('FooBarGroup is rendering!'); return ( <div> <Foo foo={foo} /> <Bar bar={bar} /> </div> ) } 複製程式碼
這時再去檢視console,我們會發現只有當foo更新的時候,Foo元件才會真正的去呼叫render方法。
PureComponent
使用PureComponent
但是如果所有的元件我們都要去自己實現shouldComponentUpdate方法, 有的時候未免會顯得很麻煩。不過好在React包裡面提供了PureComponent這個實現。
PureComponent內部實現了一個基於props和state淺比較的shouldComponentUpdate方法,基於這種淺比較,當props和state沒有發生改變時,元件的render方法便不會被呼叫到。
class Foo extends React.PureComponent { /* 我們不需要手動實現shouldCompoUpdate方法了 shouldComponentUpdate(nextProps) { return this.props.foo !== nextProps.foo; } */ render(){ console.log('Foo is rendering!'); return ( <div>{ this.props.foo }</div> ) } } 複製程式碼
PureComponent中的陷阱
我的props改變了,為什麼我的元件沒有更新?
由於PureComponent的shouldComponentUpdate是基於淺比較 shallowEqual.js 的,對於複雜物件,如果深層資料發生了改變,shouldComponentUpdate方法還是會返回false。
比如
class Foo extends PureComponent { constructor(props) { super(props); this.state = { foo: ['test'] } } handleClick = (e) => { e.preventDefault(); const foo = this.state.foo; foo.push('test'); //push是一個mutable的操作,foo的引用並沒有改變 this.setState({foo}); } render(){ console.log('Foo is rendering!'); return ( <div> <button onClick={this.handleClick}>Foo balabala</button> { this.state.foo.length } </div> ) } } 複製程式碼
上面這段程式碼當我的button進行點選時,即使我的this.state.foo發生了改變,但是我的元件卻不會有任何更新。因為我的this.state.foo( [‘test’]
)與nextState.foo( [‘test’, ‘test’]
)在shouldComponentUpdate進行的淺比較(實際使用 Object.is
)時其實是兩個相同的兩個陣列。
解決辦法
// concat會返回一個新陣列,引用發生改變, 淺比較(Object.is)會認為這是兩個不同的陣列 handleClick = (e) => { e.preventDefault(); const foo = this.state.foo; const bar = foo.concat('test'); this.setState({foo: bar}); } 複製程式碼
我的props沒有變, 為什麼我的元件更新了?
然而有的時候, 即使我是PureComponent, 在元件的props看上去沒有發生改變的時候, 元件還是被重新渲染了, interesting。
const Foo = ({foo}) => { console.log('Foo is rendering!'); return (<div>{foo}</div>); } const Bar = ({bar}) => { console.log('Bar is rendering!'); return (<div>{bar}</div>); } const FooBarGroup = ({foo, bar}) => { console.log('FooBar is rendering!'); return ( <div> <Foo foo={foo} /> <Bar bar={bar} /> </div> ) } class PureRenderer extends React.PureComponent { render(){ console.log('PureRenderer is rendering!!'); const { text } = this.props; return ( <div> {text} </div> ) } } class App extends React.Component { constructor(props) { super(props) this.state = { foo: 0, bar: 0 } } handleFooClick = (e) => { e.preventDefault(); const newFoo = this.state.foo + 1; this.setState({foo: newFoo}); } handleBarClick = (e) => { e.preventDefault(); const newBar = this.state.bar + 1; this.setState({bar: newBar}); } render() { const {foo, bar} = this.state; return ( <div className="App"> <button onClick={this.handleFooClick}>Foo</button> <button onClick={this.handleBarClick}>Bar</button> <FooBarGroup foo={foo} bar={bar} /> <PureRenderer text="blablabla" onClick={() => console.log('blablabla')} /> </div> ); } } 複製程式碼
我的PureRenderer明明已經是一個PureComponent了。但是當我點選foo或者bar button時,我還是能發現我的render方法被調到了。我似乎並沒有進行任何props的修改?
導致這種情況是因為props。onClick傳入的是 ()=>{console.log('balabalabla'
。這就意味著我每次傳入的都是一個新的函式實體。對於兩個不同的例項進行淺比較, 我總會得到這兩個物件不相等的結果。(引用比較)
解決辦法其實也很簡單, 就是持久化下來我們的物件
const onClickHandler = () => console.log('blablabla') class App extends Component { constructor(props) { super(props) this.state = { foo: 0, bar: 0 }; } render() { const {foo, bar} = this.state; return ( <div className="App"> <button onClick={this.handleFooClick}>Foo</button> <button onClick={this.handleBarClick}>Bar</button> <FooBarGroup foo={foo} bar={bar} /> <DateRerender text="blablabla" onClick={onClickHandler}/> </div> ); } 複製程式碼
這樣就能避免不必要的重複渲染了。
React 效能檢測
1. 與Chrome整合的TimeLine工具
React在開發者模式下通過呼叫User Timing API可以很好的Chrome的火炬圖進行結合來對元件的效能進行檢測。
Profiling Components with the Chrome Performance Tab

可以看到所有的React相關的函式呼叫和操作都出現在了Timeline之中。
2. Why did you update?
還有一個有意思的庫也可以幫助我們做到這些。
這個庫會提供一個高階元件, 將你的元件使用這個高階元件包裹一下, 開啟console你就能發現這個庫對於你的元件的profiling。
