Deep In React (一) 高效能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>
);
}
}
複製程式碼
當我們點選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。
3. React 16.5引入的profiling tool
React在16.5以後引入了更加強大的profilling tool,後續有時間再拿一篇文章出來講一講。 Introducing the React Profiler – React Blog
最後一點:效能可能不是那麼的重要
其實大多數時候, React已經足夠的快了。 所以當效能不是明顯的痛點的時候, 我們沒有必要花費大量的時間去強行讓我們的每個元件都做到效能的最優化。 將更多的時間花在更重要的事情上(比如重構, 更好的程式碼組織), 寫出更可維護更可讀更優美的程式碼, 才是我們每一個開發人員需要更加關注的事情。