使用PureComponent 和 memo進(jìn)行React性能優(yōu)化

前言

關(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生命周期:

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)了一小步潮针。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市倚喂,隨后出現(xiàn)的幾起案子每篷,更是在濱河造成了極大的恐慌,老刑警劉巖务唐,帶你破解...
    沈念sama閱讀 221,198評(píng)論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雳攘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡枫笛,警方通過查閱死者的電腦和手機(jī)吨灭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刑巧,“玉大人喧兄,你說我怎么就攤上這事“〕” “怎么了吠冤?”我有些...
    開封第一講書人閱讀 167,643評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)恭理。 經(jīng)常有香客問我拯辙,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評(píng)論 1 296
  • 正文 為了忘掉前任涯保,我火速辦了婚禮诉濒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘夕春。我一直安慰自己未荒,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評(píng)論 6 397
  • 文/花漫 我一把揭開白布及志。 她就那樣靜靜地躺著片排,像睡著了一般。 火紅的嫁衣襯著肌膚如雪速侈。 梳的紋絲不亂的頭發(fā)上率寡,一...
    開封第一講書人閱讀 52,156評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音锌畸,去河邊找鬼勇劣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛潭枣,可吹牛的內(nèi)容都是我干的比默。 我是一名探鬼主播,決...
    沈念sama閱讀 40,743評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼盆犁,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼命咐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起谐岁,我...
    開封第一講書人閱讀 39,659評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤醋奠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后伊佃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窜司,經(jīng)...
    沈念sama閱讀 46,200評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評(píng)論 3 340
  • 正文 我和宋清朗相戀三年航揉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了塞祈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,424評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡帅涂,死狀恐怖议薪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情媳友,我是刑警寧澤斯议,帶...
    沈念sama閱讀 36,107評(píng)論 5 349
  • 正文 年R本政府宣布,位于F島的核電站醇锚,受9級(jí)特大地震影響哼御,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評(píng)論 3 333
  • 文/蒙蒙 一艇搀、第九天 我趴在偏房一處隱蔽的房頂上張望尿扯。 院中可真熱鬧,春花似錦焰雕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至爵赵,卻和暖如春吝秕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背空幻。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工烁峭, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人秕铛。 一個(gè)月前我還...
    沈念sama閱讀 48,798評(píng)論 3 376
  • 正文 我出身青樓约郁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親但两。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鬓梅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容