インポートされたモジュール
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 %}
