Airing

Airing

哲学系学生 / 小学教师 / 程序员,个人网站: ursb.me
github
email
zhihu
medium
tg_channel
twitter_id

React Hooks Source Code Analysis (1): Class Components, Function Components, Pure Components

React source code version: v16.9.0
Source code annotation notes: airingursb/react

1 Class Component VS. Functional Component#

According to the React official website, components in React can be divided into Functional Components and Class Components.

1.1 Class Component#

Here is a familiar class component:

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

1.2 Functional Component#

Functional components are more concise:

// Functional Component
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

1.3 Stateless Component#

Functional components were previously referred to as Stateless Components, because in function components, we cannot use state; they also cannot use the component's lifecycle methods. A function component is only responsible for receiving props, rendering the DOM, and not focusing on other logic.

These components have the following characteristics:

  1. Only responsible for receiving props and rendering the DOM
  2. No state
  3. Cannot access lifecycle methods
  4. No need to declare a class: can avoid code like extends or constructor, making the syntax more concise.
  5. Will not be instantiated: therefore cannot directly pass ref (can use React.forwardRef to wrap and then pass ref).
  6. No need to explicitly declare the this keyword: in ES6 class declarations, it is often necessary to bind the function's this keyword to the current scope, but due to the nature of function declarations, we do not need to enforce binding.
  7. Better performance: because functional components do not require lifecycle management and state management, React does not need to perform certain specific checks or memory allocations, ensuring better performance.

Stateless components have simpler, clearer code and are easy to implement quickly; they are suitable for very small UI interfaces, meaning the cost of re-rendering these components is minimal.

2. Class Component VS. Pure Component#

2.1 Class Component#

The lifecycle function shouldComponentUpdate returns a boolean:

  • true: then update when props or state change
  • false: do not update

In a regular Class Component, this lifecycle function defaults to returning true, meaning that when props or state change, the class component and its child components will update.

2.2 Pure Component#

Based on the concept of purity in functional programming paradigms, if the following two conditions are met, we can call a component a Pure Component:

  1. Its return value is determined solely by its input values
  2. For the same input values, the return value is always the same.

If a React component presents the same output for the same state and props, it can be considered a pure component. For such class components, React provides the PureComponent base class. Class components implemented based on the React.PureComponent class are considered pure components.

Pure Components can reduce unnecessary updates, thereby improving performance; each update will automatically perform a simple comparison of the previous and current props and state to decide whether to update.

Next, let's look at the source code. In the entry file React.js, the Component and PureComponent base classes are exposed, coming from packages/react/src/ReactBaseClasses.js:

First, the basic Component:

/**
 * Base class helpers for the updating state of a component.
 */
function Component(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;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  // Different platforms have different updaters
  this.updater = updater || ReactNoopUpdateQueue;
}

Component.prototype.isReactComponent = {};
Component.prototype.setState = function(partialState, callback) {
  // omitted
};

Component.prototype.forceUpdate = function(callback) {
  this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};

function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;

Then the PureComponent:

/**
 * Convenience component with default shallow equality check for sCU.
 */
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;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;

As we can see, PureComponent completely inherits from Component, only adding an isPureReactComponent to the prototype chain. What is the use of this isPureReactComponent?

When scheduling updates, this property will be used to check whether the component needs to update.

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

function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
  const instance = workInProgress.stateNode;
  if (typeof instance.shouldComponentUpdate === 'function') {
    startPhaseTimer(workInProgress, 'shouldComponentUpdate');
    const shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );
    stopPhaseTimer();

    return shouldUpdate;
  }

  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

  return true;
}

If it is a PureComponent, then it will perform a shallow comparison of the new and old props and state using shallowEqual().

Now let's see what shallowEqual() does:

import is from './objectIs';

const hasOwnProperty = Object.prototype.hasOwnProperty;

/**
 * Performs equality by iterating through keys on an object and returning false
 * when any key has values which are not strictly equal between the arguments.
 * Returns true when the values of all keys are strictly equal.
 */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  // Compare the two parameters using the is function, return true if they are the same: basic data type values are the same, the same reference object indicates the same
  if (is(objA, objB)) {
    return true;
  }

  // If the two parameters are not the same, check if at least one of them is not a reference type, if so return false
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  // If both are reference type objects, continue with the comparison
  // Check if the two different reference type objects are the same
  // First, use Object.keys to get all properties of the two objects, having the same properties and each property value being the same indicates the two are the same (same also completed by the is function)
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

  if (keysA.length !== keysB.length) {
    return false;
  }

  // Test for A's keys different from B.
  for (let i = 0; i < keysA.length; i++) {
    if (
      !hasOwnProperty.call(objB, keysA[i]) ||
      !is(objA[keysA[i]], objB[keysA[i]])
    ) {
      return false;
    }
  }

  return true;
}

export default shallowEqual;

// Object.is, excludes two cases that do not meet expectations:
// 1. +0 === -0  // true
// 2. NaN === NaN // false
function is(x, y) {
  if (x === y) {
    return x !== 0 || y !== 0 || 1 / x === 1 / y;
  } else {
    return x !== x && y !== y;
  }
}

From the above analysis, we can see that when the type being compared is Object and the length of keys is equal, shallow comparison only uses Object.is() to perform a basic data type comparison of the Object's values. Therefore, if the keys contain objects, there may be cases where the comparison does not meet expectations, so shallow comparison is not suitable for nested type comparisons.

Pure components are significant for performance optimization in React as they reduce the number of rendering operations of components: if a component is a pure component, it does not need to re-render if the input has not changed. The larger the component tree, the greater the performance optimization benefits brought by pure components.

2.3 Pure Functional Component#

In sections 1.2 and 1.3, we explained how useful stateless functional components are. Now that Pure Component also has the advantage of reducing redundant rendering in terms of performance, can they be combined? Can functional components control rendering? On the surface, it seems not, because Pure Component is a class component, and its implementation is quite different from that of functional components.

However, in React 16.6, a memo function was introduced, which can give our functional components the ability to control rendering. React.memo() is a higher-order component that takes a functional component and returns a special HOC (Higher-Order Component) with memoization capabilities, able to remember the rendered component during output. (I will write a separate article about the implementation of React.memo() in the future.)

import React, { memo } from 'react';

const ToTheMoonComponent = React.memo(function MyComponent(props) {
    // only renders if props have changed
});

Memo is short for memoization, which is an optimization technique primarily used to speed up computer programs by storing the results of expensive function calls and returning the cached result when the same inputs occur again. This is precisely what React.memo() does; it checks whether the upcoming render is the same as the previous one, and if so, it skips rendering.

In previous versions, this function was called pure, provided by the recompose package, rather than being a built-in function of React.

Memoized component. There was a lot of feedback on the RFC which we agree with — "pure" naming is confusing because memoization (which is what memo does) has nothing to do with function "purity". —— Dan Abramov

3 Summary#

After introducing stateless components, functional components, pure components, and class components, let's finally discuss the principle of choosing React components: Keep it Simple Stupid (KISS):

  • If a component does not need state, use a stateless component
  • Prefer to use pure components whenever possible
  • In terms of performance: stateless functional components > class components > React.createClass()
  • Minimize props (interfaces): do not pass more props than necessary
  • If there are many conditional control flows within a component, this usually indicates that the component needs to be extracted.
  • Do not optimize prematurely; just ensure the component can be reused under current requirements, and then adapt as needed.

This section summarizes some classifications of components in React, as well as the classification methods of Smart Component and Dumb Component, but this is mainly a business classification and unrelated to technical principles, so I won't discuss it. In the next article, I will talk about the reuse methods of these components to illustrate why we need React Hooks :)

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.