在谈性能优化以前,先抛出一个问题:html
一个 React 组件,它包含两个子组件,分别是函数组件和 Class 组件。当这个 React 组件的 state 发生变化时,两个子组件的 props 并无发生变化,此时是否会致使函数子组件和 Class 子组件发生重复渲染呢?
曾拿这个问题问过很多前端求职者,但不多能给出正确的答案。下面就这个问题,浅谈下本身的认识。前端
针对上述问题,先进行一个简单的复现验证。react
App 组件包含两个子组件,分别是函数组件 ChildFunc 和类组件 ChildClass。App 组件每隔 2 秒会对自身状态 cnt 自行累加 1,用于验证两个子组件是否会发生重复渲染,具体代码逻辑以下。git
App 组件:github
import React, { Component, Fragment } from 'react'; import ReactDOM from 'react-dom'; import ChildClass from './ChildClass.jsx'; import ChildFunc from './ChildFunc.jsx'; class App extends Component { state = { cnt: 1 }; componentDidMount() { setInterval(() => this.setState({ cnt: this.state.cnt + 1 }), 2000); } render() { return ( <Fragment> <h2>疑问:</h2> <p> 一个 React 组件,它包含两个子组件,分别是函数组件和 Class 组件。当这个 React 组件的 state 发生变化时,两个子组件的 props 并无发生变化,此时是否会致使函数子组件和 Class 子组件发生重复渲染呢? </p> <div> <h3>验证(性能优化前):</h3> <ChildFunc /> <ChildClass /> </div> </Fragment> ); } } ReactDOM.render(<App />, document.getElementById('root'));
Class 组件:api
import React, { Component } from 'react'; let cnt = 0; class ChildClass extends Component { render() { cnt = cnt + 1; return <p>Class组件发生渲染次数: {cnt}</p>; } } export default ChildClass;
函数组件:数组
import React from 'react'; let cnt = 0; const ChildFunc = () => { cnt = cnt + 1; return <p>函数组件发生渲染次数: {cnt}</p>; }; export default ChildFunc;
实际验证结果代表,以下图所示,不管是函数组件仍是 Class 组件,只要父组件的 state 发生了变化,两者均会产生重复渲染。性能优化
那么该如何减小子组件发生重复渲染呢?好在 React 官方提供了 memo
组件和PureComponent
组件分别用于减小函数组件和类组件的重复渲染,具体优化逻辑以下:数据结构
Class 组件:less
import React, { PureComponent } from 'react'; let cnt = 0; class ChildClass extends PureComponent { render() { cnt = cnt + 1; return <p>Class组件发生渲染次数: {cnt}</p>; } } export default ChildClass;
函数组件:
import React, { memo } from 'react'; let cnt = 0; const OpChildFunc = () => { cnt = cnt + 1; return <p>函数组件发生渲染次数: {cnt}</p>; }; export default memo(OpChildFunc);
实际验证结果以下图所示,每当 App 组件状态发生变化时,优化后的函数子组件和类子组件均再也不产生重复渲染。
下面结合 React 源码,浅谈下 PureComponent 组件和 memo 组件的实现原理。
如下内容摘自React.PureComponent。
React.PureComponent
与 React.Component
很类似。二者的区别在于 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 更新。所以,请确保全部子组件也都是“纯”的组件。
咱们先看下在 React 中 PureComponent组件是如何定义的,如下代码摘自 React v16.9.0 中的 ReactBaseClasses.js
文件。
// ComponentDummy起桥接做用,用于PureComponent实现一个正确的原型链,其原型指向Component.prototype function ComponentDummy() {} ComponentDummy.prototype = Component.prototype; // 定义PureComponent构造函数 function PureComponent(props, context, updater) { this.props = props; this.context = context; // If a component has string refs, we will assign a different object later. this.refs = emptyObject; this.updater = updater || ReactNoopUpdateQueue; } // 将PureComponent的原型指向一个新的对象,该对象的原型正好指向Component.prototype const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy()); // 将PureComponent原型的构造函数修复为PureComponent pureComponentPrototype.constructor = PureComponent; // Avoid an extra prototype jump for these methods. Object.assign(pureComponentPrototype, Component.prototype); // 建立标识isPureReactComponent,用于标记是不是PureComponent pureComponentPrototype.isPureReactComponent = true;
名词解释:
如下代码摘自 React v16.9.0 中的 ReactFiberClassComponent.js
文件。
function checkShouldComponentUpdate( workInProgress, ctor, oldProps, newProps, oldState, newState, nextContext, ) { const instance = workInProgress.stateNode; // 若是这个组件实例自定义了shouldComponentUpdate生命周期函数 if (typeof instance.shouldComponentUpdate === 'function') { startPhaseTimer(workInProgress, 'shouldComponentUpdate'); // 执行这个组件实例自定义的shouldComponentUpdate生命周期函数 const shouldUpdate = instance.shouldComponentUpdate( newProps, newState, nextContext, ); stopPhaseTimer(); return shouldUpdate; } // 判断当前组件实例是不是PureReactComponent if (ctor.prototype && ctor.prototype.isPureReactComponent) { return ( /** * 1. 浅比较判断 oldProps 与newProps 是否相等; * 2. 浅比较判断 oldState 与newState 是否相等; */ !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) ); } return true; }
由上述代码能够看出,若是一个 PureComponent 组件自定义了shouldComponentUpdate
生命周期函数,则该组件是否进行渲染取决于shouldComponentUpdate
生命周期函数的执行结果,不会再进行额外的浅比较。若是未定义该生命周期函数,才会浅比较状态 state 和 props。
如下内容摘自React.memo。
const MyComponent = React.memo(function MyComponent(props) { /* 使用 props 渲染 */ });
React.memo
为高阶组件。它与React.PureComponent
很是类似,但它适用于函数组件,但不适用于 class 组件。
若是你的函数组件在给定相同props
的状况下渲染相同的结果,那么你能够经过将其包装在React.memo
中调用,以此经过记忆组件渲染结果的方式来提升组件的性能表现。这意味着在这种状况下,React 将跳过渲染组件的操做并直接复用最近一次渲染的结果。
默认状况下其只会对复杂对象作浅层对比,若是你想要控制对比过程,那么请将自定义的比较函数经过第二个参数传入来实现。
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 方法的返回值相反。
咱们先看下在 React 中 memo 函数是如何定义的,如下代码摘自 React v16.9.0 中的memo.js
文件。
export default function memo<Props>( type: React$ElementType, compare?: (oldProps: Props, newProps: Props) => boolean, ) { return { $$typeof: REACT_MEMO_TYPE, type, compare: compare === undefined ? null : compare, }; }
其中:
shouldcomponentupdate
生命周期函数;如下代码摘自 React v16.9.0 中的 ReactFiberBeginWork.js
文件。
function updateMemoComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, updateExpirationTime, renderExpirationTime: ExpirationTime, ): null | Fiber { /* ...省略...*/ // 判断更新的过时时间是否小于渲染的过时时间 if (updateExpirationTime < renderExpirationTime) { const prevProps = currentChild.memoizedProps; // 若是自定义了compare函数,则采用自定义的compare函数,不然采用官方的shallowEqual(浅比较)函数。 let compare = Component.compare; compare = compare !== null ? compare : shallowEqual; /** * 1. 判断当前 props 与 nextProps 是否相等; * 2. 判断即将渲染组件的引用是否与workInProgress Fiber中的引用是否一致; * * 只有二者都为真,才会退出渲染。 */ if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) { // 若是都为真,则退出渲染 return bailoutOnAlreadyFinishedWork( current, workInProgress, renderExpirationTime, ); } } /* ...省略...*/ }
由上述代码能够看出,updateMemoComponent
函数决定是否退出渲染取决于如下两点:
只有两者都为真,才会退出渲染。
其余: