今天來(lái)聊一聊 React.Component
呕臂、React.PureComponent
、React.memo
的一些區(qū)別以及使用場(chǎng)景胖腾。
一霍比、類組件定義
在 React 中幕袱,可以通過(guò)繼承 React.Component
或 React.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ì)比 prop
和 state
的方式來(lái)實(shí)現(xiàn)了該函數(shù)们豌。
如果賦予 React 組件相同的 props
和 state
,render()
函數(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é)果。僅在你的props
和state
較為簡(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)制更新組件的情況下掠抬,若 props
和 state
的變更补履,內(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ì)比oldProps
與newProps
是否相等奋早,若相等則返回false
盛霎,否則繼續(xù)往下走;2?? 接著判斷oldProps
與newProps
(此時(shí)可以確定兩者是不相等的引用值了)的第一層屬性耽装,若屬性數(shù)量或者屬性key
不一致愤炸,則認(rèn)為兩者不相等并返回true
,否則繼續(xù)往下走掉奄;3?? 判斷對(duì)應(yīng)屬性的屬性值是否相等规个,若存在不相等則返回true
,否則返回false
姓建。對(duì)于oldState
與newState
的判斷同理诞仓。注意:這里提到的返回值
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.num
和 newProps.num
時(shí)最疆,兩者的值不相等,因此會(huì)更新組件蚤告。在控制臺(tái)可以看到:
---> Parent Render.
---> Child Render.
2?? 當(dāng)我們點(diǎn)擊父組件的 Change List
按鈕時(shí)努酸,子組件不會(huì)重新渲染。因?yàn)樵趯?duì)比子組件的 oldProps.list
和 newProps.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
勋篓,useReducer
或 useContext
的 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.