抹桥的博客
言語
テーマカラー
250

Reactソースコードを紐解く - ReactChildren

目次

インポートされたモジュール

var ReactElement = require("ReactElement");

var emptyFunction = require("fbjs/lib/emptyFunction");
var invariant = require("fbjs/lib/invariant");

ReactElement モジュールを見ていきましょう。他の2つはユーティリティ関数なので、気にする必要はありません。

エクスポートされたオブジェクト

var ReactChildren = {
  forEach: forEachChildren,
  map: mapChildren,
  count: countChildren,
  toArray: toArray,
};

module.exports = ReactChildren;

この4つのAPIを順に見ていきましょう。

forEach

function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children;
  }
  var traverseContext = getPooledTraverseContext(
    null,
    null,
    forEachFunc,
    forEachContext,
  );
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  releaseTraverseContext(traverseContext);
}

入力パラメータ: children, forEachFunc, forEachContext。 まず、getPooledTraverseContext を通じてトラバースのコンテキストオブジェクト traverseContext を取得し、次に traverseAllChildren メソッドを呼び出して、渡された children のすべての子孫ノードをトラバースします。 最後に、現在の traverseContext を解放します。

getPooledTraverseContext

var POOL_SIZE = 10;
var traverseContextPool = [];
function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext,
) {
  if (traverseContextPool.length) {
    var traverseContext = traverseContextPool.pop();
    traverseContext.result = mapResult;
    traverseContext.keyPrefix = keyPrefix;
    traverseContext.func = mapFunction;
    traverseContext.context = mapContext;
    traverseContext.count = 0;
    return traverseContext;
  } else {
    return {
      result: mapResult,
      keyPrefix: keyPrefix,
      func: mapFunction,
      context: mapContext,
      count: 0,
    };
  }
}

traverseContextPool を定義し、毎回新しいオブジェクトを再作成するコストを回避します。サイズは10です。getPooledTraverseContext メソッドは、mapResult, keyPrefix, mapFunction, mapContext の4つのパラメータを受け取ります。 そして traverseContext に値を割り当て、さらにカウンタプロパティ count を追加します。

traverseAllChildren

function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }

  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
 
`traverseAllChildren` 只是个空壳,里面的 `traverseAllChildrenImpl` 才是真正的实现。
** traverseAllChildrenImpl **

function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  var type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    children = null;
  }

  if (
    children === null ||
    type === 'string' ||
    type === 'number' ||
    // The following is inlined from ReactElement. This means we can optimize
    // some checks. React Fiber also inlines this logic for similar purposes.
    (type === 'object' && children.$typeof === REACT_ELEMENT_TYPE)
  ) {
    callback(
      traverseContext,
      children,
      // If it's the only child, treat the name as if it was wrapped in an array
      // so that it's consistent if the number of children grows.
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

4つのパラメータを受け取ります。まず children パラメータの型を判断し、不正な場合はすべて children を null とみなします。 その直後のいくつかの判断は、children が単一の有効なReact要素である場合に callback 関数を実行し、1を返すことを意味します。この関数は後で再帰的に呼び出されるため、ここが再帰呼び出しの出口となります。

var child;
var nextName;
var subtreeCount = 0; // Count of children found in the current subtree.
var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

if (Array.isArray(children)) {
  for (var i = 0; i < children.length; i++) {
    child = children[i];
    nextName = nextNamePrefix + getComponentKey(child, i);
    subtreeCount += traverseAllChildrenImpl(
      child,
      nextName,
      callback,
      traverseContext,
    );
  }
} else {

渡された children が Array の場合、この Children Array をトラバースし、その中の各要素に対して現在の関数 traverseAllChildrenImpl を呼び出します。 渡されるパラメータで注意すべき点は、nextName がトラバースするReact要素に key 値を追加するために使用されることです。callback と traverseContext は現在の関数と同じ値であり、各子要素も現在の callback を適用でき、元の traverseContext にアクセスできることを保証します。

} else {
  var iteratorFn =
    (ITERATOR_SYMBOL && children[ITERATOR_SYMBOL]) ||
    children[FAUX_ITERATOR_SYMBOL];
  if (typeof iteratorFn === 'function') {
    if (__DEV__) {
      // Warn about using Maps as children
      if (iteratorFn === children.entries) {
        warning(
          didWarnAboutMaps,
          'Using Maps as children is unsupported and will likely yield ' +
            'unexpected results. Convert it to a sequence/iterable of keyed ' +
            'ReactElements instead.%s',
          getStackAddendum(),
        );
        didWarnAboutMaps = true;
      }
    }

    var iterator = iteratorFn.call(children);
    var step;
    var ii = 0;
    while (!(step = iterator.next()).done) {
      child = step.value;
      nextName = nextNamePrefix + getComponentKey(child, ii++);
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } else if (type === 'object') {
    var addendum = '';
    if (__DEV__) {
      addendum =
        ' If you meant to render a collection of children, use an array ' +
        'instead.' +
        getStackAddendum();
    }
    var childrenString = '' + children;
    invariant(
      false,
      'Objects are not valid as a React child (found: %s).%s',
      childrenString === '[object Object]'
        ? 'object with keys {' + Object.keys(children).join(', ') + '}'
        : childrenString,
      addendum,
    );
  }
}

Array ではないがイテラブルなオブジェクトの場合、上記と同様に traverseAllChildrenImpl メソッドを再帰的に呼び出します。 その他の場合、child は不正であるとみなし、エラーを発生させます。

  return subtreeCount;
}

最後に、すべての子孫要素の数を返します。 全体的に見て、traverseAllChildrenImpl メソッドの役割は、与えられた children のすべての子孫要素をトラバースし、各子孫要素で callback メソッドを呼び出し、各要素に現在のコンテキスト内で一意の key 値をパラメータとして割り当てて渡すことです。

forEach メソッドに戻ります:

traverseAllChildren(children, forEachSingleChild, traverseContext);

この行は、与えられた children のすべての子孫要素をトラバースし、それらに対して forEachSingleChild メソッドを呼び出すことを意味します。

function forEachSingleChild(bookKeeping, child, name) {
  var { func, context } = bookKeeping;
  func.call(context, child, bookKeeping.count++);
}

この渡された callback メソッド forEachSingleChild は、入力パラメータ bookKeeping、つまり traverseContext から func と context を取得し、context を func のコンテキストとして、child とカウンタ count をパラメータとして呼び出します。ここでの func は、forEachChildren の入力パラメータ forEachFunc、つまり最終ユーザーが提供する必要がある関数です。

releaseTraverseContext(traverseContext);

現在の traverseContext を解放するとは、traverseContext のすべてのプロパティを null に設定し、traverseContextPool に戻して後続の使用に供し、使用効率を向上させることを意味します。

function releaseTraverseContext(traverseContext) {
  traverseContext.result = null;
  traverseContext.keyPrefix = null;
  traverseContext.func = null;
  traverseContext.context = null;
  traverseContext.count = 0;
  if (traverseContextPool.length < POOL_SIZE) {
    traverseContextPool.push(traverseContext);
  }
}

map

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  var result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

渡された children の子孫要素に対して func を呼び出し、func の呼び出し結果のコレクションを返します。

mapIntoWithKeyPrefixInternal

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  var escapedPrefix = "";
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + "/";
  }
  var traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}

まず、prefix が渡された場合、prefix をエスケープして traverseContext の prefix key とします。 次に traverseContext オブジェクトを取得し、forEachChildren と同様に、すべての children の子孫要素をトラバースし、与えられた callback 関数を実行し、最後に traverseContext を解放します。

唯一の違いは、この callback メソッドが mapSingleChildIntoContext であることです。

mapSingleChildIntoContext

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  var { result, keyPrefix, func, context } = bookKeeping;

  var mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(
      mappedChild,
      result,
      childKey,
      emptyFunction.thatReturnsArgument,
    );
  } else if (mappedChild != null) {
    if (ReactElement.isValidElement(mappedChild)) {
      mappedChild = ReactElement.cloneAndReplaceKey(
        mappedChild,
        // Keep both the (mapped) and old keys if they differ, just as
        // traverseAllChildren used to do for objects as children
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + "/"
            : "") +
          childKey,
      );
    }
    result.push(mappedChild);
  }
}

このメソッドは上記の forEachSingleChildren と非常によく似ています。bookKeeping から result, keyPrefix, func, context を取得します。 Result は実際には mapChildren の中で最初に定義された空の配列であり、keyPrefix は mapIntoWithKeyPrefixInternal の中の escapedPrefix であり、func と context はどちらも mapChildren に対応する入力パラメータです。

まず、mappedChild をユーザーが渡した mapFunc 関数の呼び出しの戻り値として定義し、次にこの戻り値 mappedChild が Array であるかどうかを判断します。 もしそうであれば、mapIntoWithKeyPrefixInternal メソッドを繰り返し呼び出します。そうでなければ、null でなく、かつ有効なReact要素である場合、keyPrefix、ユーザーが割り当てた key、つまり mappedChild.key、および元の childkey を組み合わせて新しい key 値を生成した mappedChild のクローン要素を map の結果として result に push します。

mapChildren メソッド全体は、提供された children の各子孫要素に対して mapFunc メソッドを呼び出し、返された結果に新しい key を設定し、最後に各実行結果 mappedChild をリストに入れてユーザーに返します。

countChildren

function countChildren(children, context) {
  return traverseAllChildren(children, emptyFunction.thatReturnsNull, null);
}

これは非常にシンプルで、トラバースを通じてすべての子孫ノードの数を返すだけです。 emptyFunction.thatReturnsNull は、null を返す関数です。

toArray

function toArray(children) {
  var result = [];
  mapIntoWithKeyPrefixInternal(
    children,
    result,
    null,
    emptyFunction.thatReturnsArgument,
  );
  return result;
}

上記の mapIntoWithKeyPrefixInternal を理解していれば、ここも非常に簡単です。 emptyFunction.thatReturnsArgument は、最初の引数を返す関数です。

** mapSingleChildIntoContext **

var mappedChild = func.call(context, child, bookKeeping.count++);

つまり、この行は child 自体を返します。そしてその結果を result に入れ、最後にすべての result を呼び出し元に返します。

まとめ

ReactChildren には4つのAPIがあり、これら4つのAPIは主に traverseAllChildrenImpl と mapSingleChildIntoContext の2つのメソッドに依存しており、他のメソッドはこれらを組み合わせた呼び出しです。 もう一つ注目すべき点は、オブジェクトプール traverseContextPool が使用されていることです。個人的には、ここでは再帰呼び出しが頻繁に行われ、traverseContext オブジェクトを頻繁に新規作成する必要があるため、毎回新しいオブジェクトを作成するとヒープ内でメモリを再割り当てする必要があり、コストが高いため、パフォーマンス向上のためにオブジェクトプールが導入されたのだと考えています。

関連記事

  • {% post_link react-source-code-analyze-1 React ソースコードの浅い分析 - エントリーファイル %}
  • {% post_link react-source-code-analyze-2 React ソースコードの浅い分析 - ReactBaseClasses %}
  • {% post_link react-source-code-analyze-3 React ソースコードの浅い分析 - ReactChildren %}
  • {% post_link react-source-code-analyze-4 React ソースコードの浅い分析 - ReactElement %}
  • {% post_link react-source-code-analyze-5 React ソースコードの浅い分析 - onlyChildren %}

この記事は 2017年9月21日 に公開され、2025年10月13日 に最終更新されました。344 日が経過しており、内容が古くなっている可能性があります。

Reactソースコードを紐解く - ReactChildren
https://blog.kisnows.com/ja-JP/2017/09/21/react-source-code-analyze-3/
作者
Kisnows
公開日
2017-09-21
ライセンス
CC BY-NC-ND 4.0