前言
今天来聊一聊 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); }
根据以上源码的分析,可以得出结论:
- 若基于
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.