抹桥的博客
Language
Home
Archive
About
GitHub
Language
主题色
250
2303 文字
12 分
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 パラメータの型を判断し、不正な場合はすべて childrennull とみなします。 その直後のいくつかの判断は、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 {

渡された childrenArray の場合、この Children Array をトラバースし、その中の各要素に対して現在の関数 traverseAllChildrenImpl を呼び出します。 渡されるパラメータで注意すべき点は、nextName がトラバースするReact要素に key 値を追加するために使用されることです。callbacktraverseContext は現在の関数と同じ値であり、各子要素も現在の 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 から funccontext を取得し、contextfunc のコンテキストとして、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 をエスケープして traverseContextprefix 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 の中で最初に定義された空の配列であり、keyPrefixmapIntoWithKeyPrefixInternal の中の escapedPrefix であり、funccontext はどちらも mapChildren に対応する入力パラメータです。

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

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は主に traverseAllChildrenImplmapSingleChildIntoContext の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日 に公開され、2017年9月21日 に最終更新されました。2937 日が経過しており、内容が古くなっている可能性があります。

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