インポートされたモジュール
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日 に公開され、2017年9月21日 に最終更新されました。2937 日が経過しており、内容が古くなっている可能性があります。