前言
關(guān)于React性能優(yōu)化,有各種方法。今天败徊,我們主要使用兩個(gè)官方推出的組件模式來進(jìn)行切入,優(yōu)化點(diǎn)主要基于防止組件進(jìn)行不必要的render渲染以提升性能素征。
react原生渲染機(jī)制的性能問題
首先來看個(gè)簡(jiǎn)單的栗子集嵌,有父子兩個(gè)組件:
// 父組件
import React, { Component } from 'react';
import './App.css';
import SonComponent from './SonComponent';
class FatherComponent extends Component {
constructor() {
super();
this.state = {
fatherMsg: "who's your daddy",
sonMsg: "I'm son"
}
}
render() {
const { fatherMsg, sonMsg } = this.state;
console.log('fatherMsg', fatherMsg);
return (
<div>
<button
onClick={() => {this.setState({fatherMsg: 'father || ' + Date.now()})}}>
變換fatherMsg值</button>
<SonComponent sonMsg={sonMsg}></SonComponent>
</div>
);
}
}
export default FatherComponent;
父親組件作為容器組件萝挤,管理兩個(gè)狀態(tài),一個(gè)fatherMsg用于管理自身組件根欧,一個(gè)sonMsg用于管理子組件狀態(tài)怜珍。按鈕點(diǎn)擊事件用于修改父組件狀態(tài)。
// 子組件
import React, { Component } from 'react';
class SonComponent extends Component {
render() {
const { sonMsg } = this.props;
console.log('sonMsg', sonMsg + ' || ' + Date.now());
return (
<div>{sonMsg}</div>
)
}
}
export default SonComponent;
當(dāng)頁(yè)面初始化時(shí)凤粗,打印出了兩條數(shù)據(jù):
這很正常酥泛,頁(yè)面初始化時(shí),react進(jìn)行dom樹的建設(shè)與渲染嫌拣,數(shù)據(jù)自上而下流動(dòng)柔袁,由父到子打印出了兩個(gè)值。
當(dāng)我們點(diǎn)擊按鈕改變父組件狀態(tài)時(shí)异逐,奇怪的事情發(fā)生了:
子組件并沒有用到父組件的
fatherMsg
捶索,但是當(dāng)fatherMsg
變化時(shí),雖然子組件的props
并沒有變化灰瞻,但子組件也跟著父組件更新并重新render了:對(duì)腥例,這就是react原生渲染機(jī)制的問題:當(dāng)父組件更新時(shí),子組件無論props是否改變酝润,都進(jìn)行了重新渲染燎竖,造成了性能浪費(fèi)。
我們知道要销,對(duì)一個(gè)組件的dom進(jìn)行render构回,特別是當(dāng)dom樹較為復(fù)雜時(shí),是相當(dāng)消耗時(shí)間和性能的疏咐,應(yīng)盡量避免纤掸。以上截圖中的2ms,就是這次浪費(fèi)的時(shí)間凳鬓,而這只是一個(gè)最簡(jiǎn)單的組件茁肠,復(fù)雜的組件會(huì)消耗更多。
我們想要的缩举,是當(dāng)子組件props更新之后,才對(duì)子組件進(jìn)行重新render匹颤。
使用shouldComponentUpdate進(jìn)行人為干預(yù)
我們先來溫習(xí)一下react生命周期:
可以看到仅孩,在updating階段,有一個(gè)鉤子
shouldComponentUpdate
印蓖,它會(huì)返回一個(gè)boolean值辽慕,以控制組件是否更新。同時(shí)會(huì)接收兩個(gè)參數(shù):新的props和新的state赦肃。我們來試試:
import React, { Component } from 'react';
class SonComponent extends Component {
shouldComponentUpdate(nextProps, nextState){
console.log('當(dāng)前現(xiàn)有的props值為'+ this.props.sonMsg);
console.log('即將傳入的props值為'+ nextProps.sonMsg);
}
render() {
const { sonMsg } = this.props;
console.log('sonMsg', sonMsg + ' || ' + Date.now());
return (
<div>{sonMsg}</div>
)
}
}
export default SonComponent;
可以看到溅蛉,當(dāng)點(diǎn)擊按鈕觸發(fā)子組件更新時(shí)公浪,觸發(fā)了
shouldComponentUpdate
,但我故意沒有return船侧。所以控制臺(tái)報(bào)錯(cuò)欠气。這時(shí)子組件并沒有更新。修改返回值:
shouldComponentUpdate(nextProps,nextState){
console.log('當(dāng)前現(xiàn)有的props值為'+ this.props.sonMsg);
console.log('即將傳入的props值為'+ nextProps.sonMsg);
return this.props.sonMsg !== nextProps.sonMsg
}
這時(shí)镜撩,就實(shí)現(xiàn)了我們想要的:只有當(dāng)props變化時(shí)预柒,才更新子組件。
但是袁梗,在實(shí)際開發(fā)中宜鸯,我們的子組件往往不止有一個(gè)props。當(dāng)有多個(gè)屬性時(shí)遮怜,就需要一個(gè)個(gè)對(duì)比淋袖,會(huì)使代碼非常難看。
并且锯梁,如果是對(duì)象這種引用類型即碗,是無法通過簡(jiǎn)單的===
進(jìn)行比較的。
這時(shí)涝桅,可以采取一種取巧的方法拜姿,即序列化props后進(jìn)行比較:
shouldComponentUpdate(nextProps,nextState){
return JSON.stringify(this.props) !== JSON.stringify(nextProps)
}
但是,當(dāng)props上掛載function時(shí)冯遂,會(huì)產(chǎn)生問題:
JSON.stringify(function() {}) // undefined
并且蕊肥,由于對(duì)象是無序的,當(dāng)對(duì)象成員順序發(fā)生變化時(shí)蛤肌,序列化方法不再生效壁却。
這種方法,在大部分情況下是適用的裸准。只要注意以上說明的幾種特殊情況就好展东。
使用PureComponent
如果每個(gè)組件都需要使用shouldComponentUpdate
來控制更新,那也太low了炒俱。
react官方提供了PureComponent
模式解決默認(rèn)情況下子組件渲染策略的問題盐肃。
// 子組件
import React, { PureComponent } from 'react';
class SonComponent extends PureComponent {
render() {
const { sonMsg } = this.props;
console.log('sonMsg', sonMsg + ' || ' + Date.now());
return (
<div>{sonMsg}</div>
)
}
}
export default SonComponent;
當(dāng)父組件修改跟子組件無關(guān)的狀態(tài)時(shí),再也不會(huì)觸發(fā)自組件的更新了权悟。
用PureComponent會(huì)不會(huì)有什么缺點(diǎn)呢砸王?
剛才我們是傳入一個(gè)string字符串(基本數(shù)據(jù)類型)作為props傳遞給子組件。
現(xiàn)在我們是傳入一個(gè)object對(duì)象(引用類型)作為props傳遞給子組件峦阁∏澹看看效果如何:
// 父組件
import React, { Component } from 'react';
import './App.css';
import SonComponent from './SonComponent';
class FatherComponent extends Component {
constructor() {
super();
this.state = {
fatherMsg: "who's your daddy",
sonMsg: {
val: "I'm son"
}
}
}
render() {
const { fatherMsg, sonMsg } = this.state;
console.log('fatherMsg', fatherMsg);
return (
<div>
<button
onClick={() => {
sonMsg.val = 'son' + Date.now();
this.setState({ sonMsg })}
}>
變換sonMsg值</button>
<SonComponent sonMsg={sonMsg}></SonComponent>
</div>
);
}
}
export default FatherComponent;
當(dāng)我們點(diǎn)擊按鈕修改了sonMsg
時(shí),發(fā)現(xiàn)子組件并沒有更新榔昔,為什么驹闰?
因?yàn)镻ureComponent 對(duì)狀態(tài)的對(duì)比是淺比較亡蓉。
遇到了和shouldComponentUpdate
同樣的問題名党。關(guān)于深淺比較和拷貝奸腺,可以參考我的這篇文章诲侮。
解決這個(gè)問題,我們可以采用更換新指針或者深拷貝來解決:
this.setState(({sonMsg}) =>
{
return {
sonMsg:{
...sonMsg,
val:'son' + Date.now()
}
}
})
React.PureComponent
中的shouldComponentUpdate()
僅作對(duì)象的淺層比較骡显。如果對(duì)象中包含復(fù)雜的數(shù)據(jù)結(jié)構(gòu)疆栏,則有可能因?yàn)闊o法檢查深層的差別,產(chǎn)生錯(cuò)誤的比對(duì)結(jié)果惫谤。僅在你的 props 和 state 較為簡(jiǎn)單時(shí)壁顶,才使用React.PureComponent
,或者在深層數(shù)據(jù)結(jié)構(gòu)發(fā)生變化時(shí)調(diào)用forceUpdate()
來確保組件被正確地更新溜歪。你也可以考慮使用 immutable 對(duì)象加速嵌套數(shù)據(jù)的比較若专。
此外,React.PureComponent
中的 shouldComponentUpdate()
將跳過所有子組件樹的 prop 更新蝴猪。因此调衰,請(qǐng)確保所有子組件也都是“純”的組件。
使用forceUpdate
在react生命周期圖中自阱,我們看到一個(gè)叫forceUpdate
的鉤子嚎莉。它的作用是強(qiáng)制更新組件。當(dāng)我們沒有把握對(duì)于復(fù)雜props對(duì)比更新時(shí)沛豌,可以采用這個(gè)方法趋箩,對(duì)props進(jìn)行手動(dòng)檢測(cè)后進(jìn)行強(qiáng)制更新。
使用React.memo
上述我們花了很大篇幅加派,講的都是class組件叫确,但是隨著hooks出來后,更多的組件都會(huì)偏向于function 寫法了芍锦。React 16.6.0推出的重要功能之一竹勉,就是React.memo。
React.memo 為高階組件娄琉。它與 React.PureComponent 非常相似次乓,但它適用于函數(shù)組件,但不適用于 class 組件孽水。
如果你的函數(shù)組件在給定相同 props 的情況下渲染相同的結(jié)果檬输,那么你可以通過將其包裝在 React.memo 中調(diào)用,以此通過記憶組件渲染結(jié)果的方式來提高組件的性能表現(xiàn)匈棘。這意味著在這種情況下,React 將跳過渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果析命。
默認(rèn)情況下其只會(huì)對(duì)復(fù)雜對(duì)象做淺層對(duì)比主卫,如果你想要控制對(duì)比過程逃默,那么請(qǐng)將自定義的比較函數(shù)通過第二個(gè)參數(shù)傳入來實(shí)現(xiàn)。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 傳入 render 方法的返回結(jié)果與
將 prevProps 傳入 render 方法的返回結(jié)果一致則返回 true簇搅,
否則返回 false
*/
}
export default React.memo(MyComponent, areEqual);
此方法僅作為性能優(yōu)化的方式而存在完域。但請(qǐng)不要依賴它來“阻止”渲染,因?yàn)檫@會(huì)產(chǎn)生 bug瘩将。
此方法在函數(shù)式組件中非常有用吟税,因?yàn)樵诤瘮?shù)式組件中沒有shouldComponentUpdate方法。
結(jié)語(yǔ)
本文所進(jìn)行的react性能優(yōu)化姿现,都是針對(duì)避免子組件進(jìn)行不必要的更新來開展的肠仪。以上幾種方法都可以實(shí)現(xiàn)這種優(yōu)化,分別適合不同的場(chǎng)景备典。
但對(duì)于比較復(fù)雜類型這種棘手的問題异旧,所有方法都必須進(jìn)行一定的處理,要么在父組件進(jìn)行指針層面的更新提佣,要么在子組件進(jìn)行深度比較來控制是否更新吮蛹。
不管怎樣,在對(duì)前端代碼進(jìn)行性能優(yōu)化這項(xiàng)浩大的工程上拌屏,我們都前進(jìn)了一小步潮针。