React 源码解读之 Component & PureComponent

352 阅读5分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。

react 版本:v17.0.3

在开发中,如果我们需要定义一个 class 组件,则需要继承 React.Component,例如:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

下面,我们通过源码来看看这个 React.Component 是如何定义的。

Component

// packages/react/src/ReactBaseClasses.js

// 通过构造函数的方式声明 Component 类 
function Component(props, context, updater) {
  // props 属性,父组件传递给子组件的数据
  this.props = props;
  // context 跨层级传递数据,在组件树间传递数据
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  // refs 子到父传递数据,将节点实例挂载进来
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  // 组件的更新操作,在这里只是给 updater 赋了默认值,真正的 updater 是在 renderer 中注册的
  this.updater = updater || ReactNoopUpdateQueue;
}

// 在 Component 的原型上添加 isReactComponent 属性,表明这是一个 类 组件
Component.prototype.isReactComponent = {};

// 在原型上添加 setState 方法,使用 setState 来更新组件
// partialState 要更新的 state,可以是 Object/Function
// callback 组件更新完成后要执行的回调函数 setState({}, callback)
Component.prototype.setState = function(partialState, callback) {
// 判断 partialState 是否符合条件,如果不符合则抛出 Error
  if (
    typeof partialState !== 'object' &&
    typeof partialState !== 'function' &&
    partialState != null
  ) {
    throw new Error(
      'setState(...): takes an object of state variables to update or a ' +
        'function which returns an object of state variables.',
    );
  }
  // state 的更新机制,在 react-dom 中实现
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};


// 在Component的深层次改变但未调用setState时,
// 使用该方法,强制Component更新一次,无论 props/state 是否更新
Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

可以看到,Component 类是通过构造函数的方式来声明的,并在构造函数中定义了 props、context、refs、updater 等实例属性。

然后在 Component 类的原型上添加了一个 isReactComponent 属性,react 就是通过原型上的这个属性来区分用户定义的组件是 class组件 还是 function组件。

接下来在原型上添加了一个 setState() 方法,这个方法是执行组件更新的主要方式,它会将组件 state 的更改加入到更新队列中,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。由于setState() 并不总是立即更新组件,它会批量推迟更新,因此如果我们想要在调用 setState() 后立即读取 this.state,可以使用 setState() 的第二个参数 callback 回调函数。

最后是在原型上添加一个 forceUpdate() 方法,即使 props 或 state 没有变化,forceUpdate() 方法也会强制让组件重新渲染。

PureComponent

// packages/react/src/ReactBaseClasses.js

// 定义一个空的构造函数,其原型继承自 Component 的原型,但不包括 constructor
function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

/**
 * Convenience component with default shallow equality check for sCU.
 */
function PureComponent(props, context, updater) {
  // props 属性,父组件传递给子组件的数据
  this.props = props;
  // context 跨层级传递数据,在组件树间传递数据
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  // refs 子到父传递数据,将节点实例挂载进来
  this.refs = emptyObject;
  // 组件的更新操作,在这里只是给 updater 赋了默认值,真正的 updater 是在 renderer 中注册的
  this.updater = updater || ReactNoopUpdateQueue;
}

// 将 Component 的方法拷贝到 PureComponent 的原型上
// 用ComponentDummy的原因是为了不直接实例化一个Component实例,可以减少一些内存使用
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
// 这里相当于是 PureComponent.prototype.constructor = PureComponent
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
// 将 Component.prototype 上的方法复制到 PureComponent的原型上,
// 也就是复制到 PureComponent.prototype 上
Object.assign(pureComponentPrototype, Component.prototype);
// 在 PureComponent.prototype 上添加 isPureReactComponent 属性,
// 用以区分是 Component 还是 PureComponent
pureComponentPrototype.isPureReactComponent = true;

PureComponent 与 Component 类似,同样也是通过构造函数的方式来声明的,并同时定义了 props、context、refs、updater 等实例属性。但 PureComponent 原型上的方法都是拷贝自 Component 的原型方法。

构造函数在实例化对象时会单独开辟一块空间来存放实例属性、原型方法等,如果实例化多个对象,则会开辟多块空间,就会存在浪费内存的问题。React 在实现PureComponent的原型方法时,并没有直接实例化一个Component 实例,而是通过实例化一个空的构造函数,实现原型方法的共享,从而减少了内存的使用。

最后在 PureComponent 的原型上添加一个 isPureReactComponent 属性,用来区分是 Component 还是 PureComponent 。

两者的区别

Component 与 PureComponent 的区别在于 Component 并未实现 shouldComponentUpdate(),而 PureComponent 中以浅层对比 props 和 state 的方式实现了该函数。其实现源码在 react-reconciler/src/ReactFiberClassComponent.new.js 文件中的 checkShouldComponentUpdate() 函数中:

// react-reconciler/src/ReactFiberClassComponent.new.js

function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
  
    
	// 如果是PureComponent,调用 shallowEqual,浅比较 props 或 state,决定是否需要更新组件
  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

  return true;
}

checkShouldComponentUpdate() 函数中,通过判断组件实例的原型上是否有isPureReactComponent属性,如果有,说明当前组件实例是一个 PureComponent ,则通过 shallowEqual() 方法来浅比较 props 或 state,决定是否需要更新组件。

传送门:React PureComponent 中的浅比较

总结

本文从源码上介绍了 Component 和 PureComponent。两者的实现方式方式相似,都是通过构造函数声明类,并定义了 props、context、refs、updater等实例属性。不同的是 PureComponent 的原型方法拷贝自 Component,并且Component并未实现 shouldComponentUpdate(),PureComponent通过浅比较props 和 state的方式实现了该函数。

因此,我们可以认为 PureComponent 是自带了 shouldComponentUpdate 更新机制的 Component,在某些情况下使用 PureComponent 可以提高性能。