Airing

Airing

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

React Hooks ソースコード解析(1):クラスコンポーネント、関数コンポーネント、純コンポーネント

React ソースコードバージョン: v16.9.0
ソースコード注釈ノート:airingursb/react

1 クラスコンポーネント VS. 関数コンポーネント#

React 公式サイトによると、React のコンポーネントは ** 関数コンポーネント(Functional Component)クラスコンポーネント(Class Component)** に分けられます。

1.1 クラスコンポーネント#

これは私たちがよく知っているクラスコンポーネントです:

// クラスコンポーネント
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

1.2 関数コンポーネント#

関数コンポーネントはさらに簡潔です:

// 関数コンポーネント
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

1.3 ステートレスコンポーネント#

関数コンポーネントは以前は ** ステートレスコンポーネント(Stateless Component)** とも呼ばれていました。なぜなら、関数コンポーネントではstateを使用できず、コンポーネントのライフサイクルメソッドも使用できないからです。関数コンポーネントはpropsを受け取り、DOM をレンダリングするだけで、他のロジックには関与しません。

この種のコンポーネントには以下の特徴があります:

  1. propsを受け取り、DOM をレンダリングするだけ
  2. stateがない
  3. ライフサイクルメソッドにアクセスできない
  4. クラスを宣言する必要がない:extendsconstructorのようなコードを避けられ、文法がより簡潔です。
  5. インスタンス化されない:したがって、ref を直接渡すことはできません(React.forwardRefでラップしてから ref を渡すことができます)。
  6. this キーワードを明示的に宣言する必要がない:ES6 のクラス宣言では、関数の this キーワードを現在のスコープにバインドする必要がありますが、関数式の特性により、強制的にバインドする必要はありません。
  7. より良いパフォーマンス:関数コンポーネントではライフサイクル管理や状態管理が不要なため、React は特定のチェックやメモリ割り当てを行う必要がなく、より良いパフォーマンスを保証します。

ステートレスコンポーネントのコードはよりシンプルで明確であり、迅速に実装できます。これらは非常に小さな UI インターフェースに適しています。つまり、これらのコンポーネントの再レンダリングのコストは非常に小さいです。

2. クラスコンポーネント VS. ピュアコンポーネント#

2.1 クラスコンポーネント#

ライフサイクル関数shouldComponentUpdateはブール値を返します:

  • true: propsまたはstateが変更されたときに更新します
  • false: 更新しません

通常のクラスコンポーネントでは、このライフサイクル関数はデフォルトで true を返します。つまり、propsまたはstateが変更されたときにクラスコンポーネントとその子コンポーネントが更新されます。

2.2 ピュアコンポーネント#

関数型プログラミングの純度の概念に基づき、以下の 2 つの条件を満たす場合、コンポーネントはピュアコンポーネントと見なされます:

  1. その戻り値は入力値のみに依存する
  2. 同じ入力値に対して、戻り値は常に同じである。

React コンポーネントが同じstatepropsで同じ出力を生成する場合、それはピュアコンポーネントと見なすことができます。このようなクラスコンポーネントには、React が提供するPureComponent基底クラスがあります。React.PureComponentクラスに基づいて実装されたクラスコンポーネントはピュアコンポーネントと見なされます。

ピュアコンポーネントは不必要な更新を減らし、パフォーマンスを向上させます。更新のたびに、更新前後のpropsstateを簡単に比較して、更新するかどうかを決定します。

次にソースコードを見てみましょう。エントリーファイルReact.jsでは、ComponentPureComponentの 2 つの基底クラスが公開されています。これらはpackages/react/src/ReactBaseClasses.jsから来ています:

まず基本的なComponent

/**
 * コンポーネントの状態を更新するための基本クラスヘルパー。
 */
function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // コンポーネントが文字列refを持つ場合、後で異なるオブジェクトを割り当てます。
  this.refs = emptyObject;
  // デフォルトのアップデーターを初期化しますが、実際のものは
  // レンダラーによって注入されます。
  // プラットフォームによってアップデーターは異なります
  this.updater = updater || ReactNoopUpdateQueue;
}

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

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

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

次にPureComponent

/**
 * sCUのためのデフォルトの浅い等価性チェックを持つ便利なコンポーネント。
 */
function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  // コンポーネントが文字列refを持つ場合、後で異なるオブジェクトを割り当てます。
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// これらのメソッドのために余分なプロトタイプジャンプを避けます。
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;

PureComponentは完全にComponentから継承されており、原型チェーンにisPureReactComponentが追加されているだけです。このisPureReactComponentは何のためにあるのでしょうか?

更新をスケジュールする際に、この属性はコンポーネントが更新する必要があるかどうかをチェックするために使用されます。

// 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;
}

PureComponentの場合、新旧のpropsstateshallowEqual()で浅く比較します。

では、shallowEqual()が何をしているのか見てみましょう:

import is from './objectIs';

const hasOwnProperty = Object.prototype.hasOwnProperty;

/**
 * オブジェクトのキーを反復処理して等価性を実行し、引数間で値が厳密に等しくないキーがある場合はfalseを返します。
 * すべてのキーの値が厳密に等しい場合はtrueを返します。
 */
function shallowEqual(objA: mixed, objB: mixed): boolean {
  // is関数を使って2つの引数を比較し、同じであればtrueを返します:基本データ型の値が同じであれば、同じ参照オブジェクトも同じを示します
  if (is(objA, objB)) {
    return true;
  }

  // 2つの引数が異なる場合、少なくとも1つが参照型でないかどうかを判断し、そうであればfalseを返します
  if (
    typeof objA !== 'object' ||
    objA === null ||
    typeof objB !== 'object' ||
    objB === null
  ) {
    return false;
  }

  // 2つが参照型オブジェクトの場合、比較を続けます
  // 2つの異なる参照型オブジェクトが同じかどうかを判断します
  // まずObject.keysを使って2つのオブジェクトのすべてのプロパティを取得し、同じプロパティを持ち、各プロパティの値が同じであれば2つのオブジェクトは同じです(同じもis関数で確認します)
  const keysA = Object.keys(objA);
  const keysB = Object.keys(objB);

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

  // Aのキーが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は、===の2つの予期しないケースを除外します:
// 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;
  }
}

上記の分析から、比較するタイプがオブジェクトであり、キーの長さが等しい場合、浅い比較はObject.is()を使用してオブジェクトの値を基本データ型の比較を行うだけです。したがって、キーの中にオブジェクトがある場合、比較が予期しない結果になる可能性があるため、浅い比較はネストされたタイプの比較には適用できません

ピュアコンポーネントは React のパフォーマンス最適化に重要な意味を持ちます。コンポーネントのレンダリング操作の回数を減らします:もしコンポーネントがピュアコンポーネントであり、入力が変更されていなければ、そのコンポーネントは再レンダリングする必要がありません。コンポーネントツリーが大きくなるほど、ピュアコンポーネントによるパフォーマンス最適化の利点は大きくなります。

2.3 ピュア関数コンポーネント#

1.2 と 1.3 で、ステートレス関数コンポーネントがどれほど便利であるかを説明しました。今、ピュアコンポーネントもパフォーマンス上の重複レンダリングを減らす利点がありますが、これらは組み合わせて使用できるのでしょうか?関数コンポーネントはレンダリングを制御できるのでしょうか?表面的には不可能に見えます。なぜなら、ピュアコンポーネントはクラスコンポーネントであり、関数コンポーネントの実装とは無関係だからです。

しかし、React 16.6 ではmemo関数が提供され、関数コンポーネントにもレンダリング制御の能力を持たせることができますReact.memo()は高階コンポーネント(HOC)で、関数コンポーネントを受け取り、レンダリングされたコンポーネントを記憶する特別な HOC を返します。(React.memo()の実装については、後で別の記事で詳しく説明します。)

import React, { memo } from 'react';

const ToTheMoonComponent = React.memo(function MyComponent(props) {
    // propsが変更された場合のみレンダリングされます
});

memo はメモ化の略で、メモは最適化技術の一種で、主に高価な関数呼び出しの結果を保存することでコンピュータプログラムを加速し、同じ入力が再度発生した場合にキャッシュされた結果を返します。そして、これがまさにReact.memo()が行う実装です。これは、次のレンダリングが前のレンダリングと同じかどうかをチェックし、同じであればレンダリングを保持します。

以前のバージョンでは、この関数の名前はpureで、recomposeパッケージによって提供されていましたが、React の組み込み関数ではありませんでした。

メモ化されたコンポーネント。RFC に対する多くのフィードバックがあり、私たちはそれに同意します — "pure" という名前は混乱を招きます。なぜなら、メモ化(memo が行うこと)は関数の "純度" とは関係がないからです。—— ダン・アブラモフ

3 小節#

ステートレスコンポーネント、関数コンポーネント、ピュアコンポーネント、クラスコンポーネントについて説明した後、最後に React コンポーネントを選択する際のKeep it Simple Stupid (KISS)原則について紹介します:

  • コンポーネントが状態を必要としない場合は、ステートレスコンポーネントを使用します
  • 可能な限りピュアコンポーネントを使用します
  • パフォーマンスの観点から:ステートレス関数コンポーネント > クラスコンポーネント > React.createClass ()
  • props(インターフェース)を最小限に:要求以上の props を渡さない
  • コンポーネント内部に多くの条件制御フローが存在する場合、通常はコンポーネントを抽出する必要があります。
  • 早すぎる最適化は避け、コンポーネントが現在の要求に対して再利用可能であることを求め、その後柔軟に対応します。

この節では、React におけるコンポーネントの分類と、スマートコンポーネントとダムコンポーネントの分類方法についてまとめましたが、これは主にビジネス上の分類であり、技術原理とは関係がないため、ここでは触れません。次の記事では、これらのコンポーネントの再利用方法について説明し、なぜ React Hooks が必要なのかを示します:)

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。