細(xì)讀 React | PureComponet

配圖源自 Freepik

今天來(lái)聊一聊 React.Component呕臂、React.PureComponentReact.memo 的一些區(qū)別以及使用場(chǎng)景胖腾。

一霍比、類組件定義

在 React 中幕袱,可以通過(guò)繼承 React.ComponentReact.PureComponent 來(lái)定義 Class 組件:

import React, { Component, PureComponent } from 'react'

class Comp extends Component {
  // ...
}

class PureComp extends PureComponent {
  // ...
}

兩者很相似,區(qū)別在于 React.Component 并未實(shí)現(xiàn) shouldComponentUpdate()悠瞬,而 React.PureComponent 中以淺層對(duì)比 propstate 的方式來(lái)實(shí)現(xiàn)了該函數(shù)们豌。

如果賦予 React 組件相同的 propsstaterender() 函數(shù)會(huì)渲染相同的內(nèi)容浅妆,那么在某些情況下使用 React.PureComponent 可提高性能望迎。

注意:React.PureComponent 中的 shouldComponentUpdate() 僅作對(duì)象的淺層比較。如果對(duì)象中包含復(fù)雜的數(shù)據(jù)結(jié)構(gòu)凌外,則有可能因?yàn)闊o(wú)法檢查深層的差別辩尊,產(chǎn)生錯(cuò)誤的比對(duì)結(jié)果。僅在你的 propsstate 較為簡(jiǎn)單時(shí)康辑,才使用 React.PureComponent摄欲,或者在深層數(shù)據(jù)結(jié)構(gòu)發(fā)生變化時(shí)調(diào)用 forceUpdate() 來(lái)確保組件被正確地更新。你也可以考慮使用 immutable 對(duì)象加速嵌套數(shù)據(jù)的比較疮薇。

此外胸墙,React.PureComponent 中的 shouldComponentUpdate() 將跳過(guò)所有子組件樹(shù)的 prop 更新。因此按咒,請(qǐng)確保所有子組件也都是“純”的組件迟隅。

二、淺層對(duì)比實(shí)現(xiàn)

我們來(lái)看下源碼励七,它們是如何“淺層對(duì)比”的智袭?

首先,在非強(qiáng)制更新組件的情況下掠抬,若 propsstate 的變更补履,內(nèi)部都會(huì)觸發(fā) checkShouldComponentUpdate 方法來(lái)判斷是否重新渲染組件。若使用 forceUpdate() 強(qiáng)制更新組件的話剿另,則會(huì)跳過(guò)該方法。

// checkHasForceUpdateAfterProcessing 方法用于判斷是否強(qiáng)制更新
// 若不是強(qiáng)制更新,則會(huì)根據(jù) checkShouldComponentUpdate 方法判斷是否應(yīng)該更新組件
var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);
function checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext) {
  var instance = workInProgress.stateNode;

  // 若自實(shí)現(xiàn)了 shouldComponentUpdate 方法雨女,則不會(huì)跑到后面的步驟
  if (typeof instance.shouldComponentUpdate === 'function') {
    startPhaseTimer(workInProgress, 'shouldComponentUpdate');
    var shouldUpdate = instance.shouldComponentUpdate(newProps, newState, nextContext);
    stopPhaseTimer();

    {
      !(shouldUpdate !== undefined) ? warningWithoutStack$1(false, '%s.shouldComponentUpdate(): Returned undefined instead of a ' + 'boolean value. Make sure to return true or false.', getComponentName(ctor) || 'Component') : void 0;
    }

    return shouldUpdate;
  }

  // 關(guān)鍵是這里:
  // 在 React 組件未實(shí)現(xiàn) shouldComponentUpdate 前提下谚攒,
  // 可通過(guò) isPureReactComponent 判斷是否為 PureComponent 組件的原因是構(gòu)造函數(shù)里設(shè)置了該屬性的值為 true。
  // 使用 shallowEqual 方法來(lái)判斷組件屬性和狀態(tài)時(shí)是否發(fā)生了變化氛堕,若兩種均是“相等”馏臭,則返回 false,即不更新組件讼稚,否則會(huì)觸發(fā)組件的 render() 方法以更新組件括儒。
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState);
  }

  return true;
}

再看下 shallowEqual 的實(shí)現(xiàn),不難:

function shallowEqual(objA, objB) {
  // is$1 相當(dāng)于 ES6 的 Object.is() 方法锐想,比較兩個(gè)操作數(shù)是否相等
  if (is$1(objA, objB)) {
    return true;
  }

  // 講過(guò)上一步的排除之后帮寻,若 objA 或 ObjB 的值是“非引用類型”或 null,則可以確定 objA 與 objB 是不相等的赠摇。
  if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
    return false;
  }

  // 走到這步固逗,說(shuō)明 objA 和 objB 是兩個(gè)不同的引用類型的值
  var keysA = Object.keys(objA);
  var keysB = Object.keys(objB);
  
  // 比較兩者的屬性數(shù)量是否一致,若不一致藕帜,則可確定兩者是不相等的
  if (keysA.length !== keysB.length) {
    return false;
  } // Test for A's keys different from B.

  // 這里只遍歷最外層的屬性是否一致
  for (var i = 0; i < keysA.length; i++) {
    // hasOwnProperty$2 即 Object.prototype.hasOwnProperty烫罩;
    // 先比較 objA 的屬性,在 objB 屬性有沒(méi)有洽故,若無(wú)說(shuō)明兩者不相等贝攒,否則接著再判斷同一屬性值是否相等,
    // 這判斷就比較簡(jiǎn)單了:Object.is() 是使用全等判斷的时甚,并認(rèn)為 NaN === NaN 和 +0 !== -0 的隘弊。
    if (!hasOwnProperty$2.call(objB, keysA[i]) || !is$1(objA[keysA[i]], objB[keysA[i]])) {
      return false;
    }
  }

  // 否則,返回 true撞秋,認(rèn)為它們相等长捧。
  return true;
}

默認(rèn)淺層對(duì)比方法,相當(dāng)于:

shouldComponentUpdate(nextProps, nextState) {
  return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}

關(guān)于 Object.is() 解決了什么“奇葩”問(wèn)題吻贿,可以看此前的一篇文章:JavaScript 相等比較詳解 的第三節(jié)內(nèi)容串结。

根據(jù)以上源碼的分析,可以得出結(jié)論:

  • 若基于 React.PureComponent 的組件自實(shí)現(xiàn)了 shouldComponentUpdate() 方法舅列,則會(huì)跳過(guò)默認(rèn)的“淺層對(duì)比”肌割,可以理解為覆蓋了默認(rèn)的 shouldComponentUpdate() 方法。帐要。

  • 從源碼可知把敞,React.Component “未實(shí)現(xiàn)” shouldComponentUpdate() 是因?yàn)閮?nèi)部返回了 true 而已。

  • React.PureComponent 的淺層對(duì)比榨惠,主要分為三步判斷:1?? 對(duì)比 oldPropsnewProps 是否相等奋早,若相等則返回 false盛霎,否則繼續(xù)往下走;2?? 接著判斷 oldPropsnewProps (此時(shí)可以確定兩者是不相等的引用值了)的第一層屬性耽装,若屬性數(shù)量或者屬性 key 不一致愤炸,則認(rèn)為兩者不相等并返回 true,否則繼續(xù)往下走掉奄;3?? 判斷對(duì)應(yīng)屬性的屬性值是否相等规个,若存在不相等則返回 true,否則返回 false姓建。對(duì)于 oldStatenewState 的判斷同理诞仓。

    注意:這里提到的返回值 true/false 是指 !shalldowEqual() 的結(jié)果,相當(dāng)于 shouldComponentUpdate() 的返回值

三速兔、示例及注意事項(xiàng)

基于以上結(jié)論墅拭,來(lái)看幾個(gè)示例吧。

先明確幾點(diǎn):

  • 使用 setState() 來(lái)更新?tīng)顟B(tài)憨栽,無(wú)論狀態(tài)值是否真的發(fā)生了改變帜矾,都會(huì)產(chǎn)生一個(gè)全新的對(duì)象,即 oldState !== newState屑柔。
  • 組件的 props 對(duì)象是 readonly(只讀)的屡萤,React 會(huì)保護(hù)它不被更改,否則會(huì)出錯(cuò)掸宛。
  • 每次父組件的重新渲染死陆,子組件的 props 都會(huì)是一個(gè)全新的對(duì)象,即 oldProps !== newProps唧瘾。
  • 一般情況下措译,組件實(shí)例的 props 值幾乎都是一個(gè)引用類型的值,即對(duì)象饰序,我還沒(méi)想到有什么場(chǎng)景會(huì)出現(xiàn) null 的情況领虹。而組件實(shí)例的 state 值則可能是對(duì)象或 null,后者即無(wú)狀態(tài)的類組件求豫,當(dāng)然這種情況下應(yīng)可能使用函數(shù)組件塌衰。
// 父組件
class Parent extends React.Component {
  state = {
    number: 0, // 原始類型
    list: [] // 引用類型
  }

  changeList() {
    const { list } = this.state
    list.push(0)
    this.setState({ list })
  }

  changeNumber() {
    this.setState({ number: this.state.number + 1 })
  }

  render() {
    console.log('---> Parent Render.')
    return (
      <>
        <button onClick={this.changeNumber.bind(this)}>Change Number</button>
        <button onClick={this.changeList.bind(this)}>Change List</button>
        <Child num={this.state.number} lists={this.state.list} />
      </>
    )
  }
}


// 子組件
class Child extends React.PureComponent {
  state = {
    name: 'child'
  }

  render() {
    console.log('---> Child Render.')
    return (
      <>
        <div>Child Component.</div>
      </>
    )
  }
}

1?? 當(dāng)我們點(diǎn)擊父組件的 Change Number 按鈕時(shí),子組件會(huì)重新渲染蝠嘉。因?yàn)樵趯?duì)比子組件的 oldProps.numnewProps.num 時(shí)最疆,兩者的值不相等,因此會(huì)更新組件蚤告。在控制臺(tái)可以看到:

---> Parent Render.
---> Child Render.

2?? 當(dāng)我們點(diǎn)擊父組件的 Change List 按鈕時(shí)努酸,子組件不會(huì)重新渲染。因?yàn)樵趯?duì)比子組件的 oldProps.listnewProps.list 時(shí)杜恰,它們都是引用類型获诈,且兩者在內(nèi)存中的地址是一致的仍源,而且不會(huì)更深層次地去比較了,因此 React 認(rèn)為它倆是相等的烙荷,因此不會(huì)更新組件镜会。在控制臺(tái)只看到:

---> Parent Render.

當(dāng)然,這一點(diǎn)也是 React.PureComponent 的局限性终抽,因此它應(yīng)該應(yīng)用于一些數(shù)據(jù)結(jié)構(gòu)較為簡(jiǎn)單的展示類組件。

另外桶至,React.PureComponent 中的 shouldComponentUpdate() 將跳過(guò)所有子組件樹(shù)的 prop 更新昼伴。因此,請(qǐng)確保所有子組件也都是“純”的組件镣屹。

四圃郊、延伸 React.memo

如果在函數(shù)組件中,想要擁有類似 React.PureComponent 的性能優(yōu)化女蜈,可以使用 React.memo持舆。

const MyComponent = React.memo(function MyComponent(props) {
  /* 使用 props 渲染 */
})

React.memo高階組件

如果你的組件在相同 props 的情況下渲染相同的結(jié)果伪窖,那么你可以通過(guò)將其包裝在 React.memo 中調(diào)用逸寓,以此通過(guò)記憶組件渲染結(jié)果的方式來(lái)提高組件的性能表現(xiàn)。這意味著在這種情況下覆山,React 將跳過(guò)渲染組件的操作并直接復(fù)用最近一次渲染的結(jié)果竹伸。

React.memo 僅檢查 props 變更。如果函數(shù)組件被 React.memo 包裹簇宽,且其實(shí)現(xiàn)中擁有 useState勋篓,useReduceruseContext 的 Hook,當(dāng) context 發(fā)生變化時(shí)魏割,它仍會(huì)重新渲染譬嚣。

默認(rèn)情況下其只會(huì)對(duì)復(fù)雜對(duì)象做淺層對(duì)比,如果你想要控制對(duì)比過(guò)程钞它,那么請(qǐng)將自定義的比較函數(shù)通過(guò)第二個(gè)參數(shù)傳入來(lái)實(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)不要依賴它來(lái)“阻止”渲染盐股,因?yàn)檫@會(huì)產(chǎn)生 bug。

注意耻卡,與 class 組件中 shouldComponentUpdate() 方法不同的是疯汁,如果 props 相等,areEqual 會(huì)返回 true卵酪;如果 props 不相等幌蚊,則返回 false谤碳。這與 shouldComponentUpdate 方法的返回值相反。

簡(jiǎn)單來(lái)說(shuō)溢豆,若需要更新組件蜒简,那么 areEqual 方法請(qǐng)返回 false,否則返回 true漩仙。

The end.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末搓茬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子队他,更是在濱河造成了極大的恐慌卷仑,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件麸折,死亡現(xiàn)場(chǎng)離奇詭異锡凝,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)垢啼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門窜锯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人芭析,你說(shuō)我怎么就攤上這事锚扎。” “怎么了放刨?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵工秩,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我进统,道長(zhǎng)助币,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任螟碎,我火速辦了婚禮眉菱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘掉分。我一直安慰自己俭缓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布酥郭。 她就那樣靜靜地躺著华坦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪不从。 梳的紋絲不亂的頭發(fā)上惜姐,一...
    開(kāi)封第一講書(shū)人閱讀 51,488評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼歹袁。 笑死坷衍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的条舔。 我是一名探鬼主播枫耳,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼孟抗!你這毒婦竟也來(lái)了迁杨?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤夸浅,失蹤者是張志新(化名)和其女友劉穎仑最,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體帆喇,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年亿胸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坯钦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡侈玄,死狀恐怖婉刀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情序仙,我是刑警寧澤突颊,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站潘悼,受9級(jí)特大地震影響律秃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜治唤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一棒动、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宾添,春花似錦船惨、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至扛邑,卻和暖如春怜浅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鹿榜。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工海雪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留锦爵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓奥裸,卻偏偏與公主長(zhǎng)得像险掀,于是被迫代替她去往敵國(guó)和親寂呛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子枝缔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

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