
今天来聊一聊 React.Component
、React.PureComponent
、React.memo
的一些区别以及使用场景。
一、类组件定义
在 React 中,可以通过继承 React.Component
或 React.PureComponent
来定义 Class 组件:
import React, { Component, PureComponent } from 'react'
class Comp extends Component {
// ...
}
class PureComp extends PureComponent {
// ...
}
两者很相似,区别在于 React.Component
并未实现 shouldComponentUpdate()
,而 React.PureComponent
中以浅层对比 prop
和 state
的方式来实现了该函数。
如果赋予 React 组件相同的 props
和 state
,render()
函数会渲染相同的内容,那么在某些情况下使用 React.PureComponent
可提高性能。
注意:
React.PureComponent
中的shouldComponentUpdate()
仅作对象的浅层比较。如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。仅在你的props
和state
较为简单时,才使用React.PureComponent
,或者在深层数据结构发生变化时调用forceUpdate()
来确保组件被正确地更新。你也可以考虑使用 immutable 对象加速嵌套数据的比较。此外,
React.PureComponent
中的shouldComponentUpdate()
将跳过所有子组件树的prop
更新。因此,请确保所有子组件也都是“纯”的组件。
二、浅层对比实现
我们来看下源码,它们是如何“浅层对比”的?
首先,在非强制更新组件的情况下,若 props
和 state
的变更,内部都会触发 checkShouldComponentUpdate
方法来判断是否重新渲染组件。若使用 forceUpdate()
强制更新组件的话,则会跳过该方法。
// checkHasForceUpdateAfterProcessing 方法用于判断是否强制更新
// 若不是强制更新,则会根据 checkShouldComponentUpdate 方法判断是否应该更新组件
var shouldUpdate = checkHasForceUpdateAfterProcessing() || checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext);
function checkShouldComponentUpdate(workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext) {
var instance = workInProgress.stateNode;
// 若自实现了 shouldComponentUpdate 方法,则不会跑到后面的步骤
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;
}
// 关键是这里:
// 在 React 组件未实现 shouldComponentUpdate 前提下,
// 可通过 isPureReactComponent 判断是否为 PureComponent 组件的原因是构造函数里设置了该属性的值为 true。
// 使用 shallowEqual 方法来判断组件属性和状态时是否发生了变化,若两种均是“相等”,则返回 false,即不更新组件,否则会触发组件的 render() 方法以更新组件。
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState);
}
return true;
}
再看下 shallowEqual
的实现,不难:
function shallowEqual(objA, objB) {
// is$1 相当于 ES6 的 Object.is() 方法,比较两个操作数是否相等
if (is$1(objA, objB)) {
return true;
}
// 讲过上一步的排除之后,若 objA 或 ObjB 的值是“非引用类型”或 null,则可以确定 objA 与 objB 是不相等的。
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
return false;
}
// 走到这步,说明 objA 和 objB 是两个不同的引用类型的值
var keysA = Object.keys(objA);
var keysB = Object.keys(objB);
// 比较两者的属性数量是否一致,若不一致,则可确定两者是不相等的
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 属性有没有,若无说明两者不相等,否则接着再判断同一属性值是否相等,
// 这判断就比较简单了:Object.is() 是使用全等判断的,并认为 NaN === NaN 和 +0 !== -0 的。
if (!hasOwnProperty$2.call(objB, keysA[i]) || !is$1(objA[keysA[i]], objB[keysA[i]])) {
return false;
}
}
// 否则,返回 true,认为它们相等。
return true;
}
默认浅层对比方法,相当于:
shouldComponentUpdate(nextProps, nextState) {
return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState);
}
关于
Object.is()
解决了什么“奇葩”问题,可以看此前的一篇文章:JavaScript 相等比较详解 的第三节内容。
根据以上源码的分析,可以得出结论:
-
若基于
React.PureComponent
的组件自实现了shouldComponentUpdate()
方法,则会跳过默认的“浅层对比”,可以理解为覆盖了默认的 shouldComponentUpdate() 方法。。 -
从源码可知,
React.Component
“未实现”shouldComponentUpdate()
是因为内部返回了true
而已。 -
React.PureComponent
的浅层对比,主要分为三步判断:1️⃣ 对比oldProps
与newProps
是否相等,若相等则返回false
,否则继续往下走;2️⃣ 接着判断oldProps
与newProps
(此时可以确定两者是不相等的引用值了)的第一层属性,若属性数量或者属性key
不一致,则认为两者不相等并返回true
,否则继续往下走;3️⃣ 判断对应属性的属性值是否相等,若存在不相等则返回true
,否则返回false
。对于oldState
与newState
的判断同理。注意:这里提到的返回值
true/false
是指!shalldowEqual()
的结果,相当于shouldComponentUpdate()
的返回值
三、示例及注意事项
基于以上结论,来看几个示例吧。
先明确几点:
- 使用
setState()
来更新状态,无论状态值是否真的发生了改变,都会产生一个全新的对象,即oldState !== newState
。- 组件的
props
对象是readonly
(只读)的,React 会保护它不被更改,否则会出错。- 每次父组件的重新渲染,子组件的
props
都会是一个全新的对象,即oldProps !== newProps
。- 一般情况下,组件实例的
props
值几乎都是一个引用类型的值,即对象,我还没想到有什么场景会出现null
的情况。而组件实例的state
值则可能是对象或null
,后者即无状态的类组件,当然这种情况下应可能使用函数组件。
// 父组件
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️⃣ 当我们点击父组件的 Change Number
按钮时,子组件会重新渲染。因为在对比子组件的 oldProps.num
和 newProps.num
时,两者的值不相等,因此会更新组件。在控制台可以看到:
---> Parent Render.
---> Child Render.
2️⃣ 当我们点击父组件的 Change List
按钮时,子组件不会重新渲染。因为在对比子组件的 oldProps.list
和 newProps.list
时,它们都是引用类型,且两者在内存中的地址是一致的,而且不会更深层次地去比较了,因此 React 认为它俩是相等的,因此不会更新组件。在控制台只看到:
---> Parent Render.
当然,这一点也是 React.PureComponent
的局限性,因此它应该应用于一些数据结构较为简单的展示类组件。
另外,React.PureComponent
中的 shouldComponentUpdate()
将跳过所有子组件树的 prop
更新。因此,请确保所有子组件也都是“纯”的组件。
四、延伸 React.memo
如果在函数组件中,想要拥有类似 React.PureComponent
的性能优化,可以使用 React.memo
。
const MyComponent = React.memo(function MyComponent(props) {
/* 使用 props 渲染 */
})
React.memo
为高阶组件。
如果你的组件在相同 props
的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo
中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
React.memo
仅检查 props
变更。如果函数组件被 React.memo
包裹,且其实现中拥有 useState
,useReducer
或 useContext
的 Hook,当 context
发生变化时,它仍会重新渲染。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。
function MyComponent(props) {
/* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
/*
如果把 nextProps 传入 render 方法的返回结果与
将 prevProps 传入 render 方法的返回结果一致则返回 true,
否则返回 false
*/
}
export default React.memo(MyComponent, areEqual)
此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。
注意,与 class 组件中
shouldComponentUpdate()
方法不同的是,如果props
相等,areEqual
会返回true
;如果props
不相等,则返回false
。这与shouldComponentUpdate
方法的返回值相反。简单来说,若需要更新组件,那么
areEqual
方法请返回false
,否则返回true
。
The end.
网友评论