Imported Modules
var ReactElement = require("ReactElement");
var emptyFunction = require("fbjs/lib/emptyFunction");
var invariant = require("fbjs/lib/invariant");
Let’s look at the ReactElement
module; the other two are utility functions that we don’t need to concern ourselves with.
Exported Objects
var ReactChildren = {
forEach: forEachChildren,
map: mapChildren,
count: countChildren,
toArray: toArray,
};
module.exports = ReactChildren;
Let’s examine these four APIs one by one.
forEach
function forEachChildren(children, forEachFunc, forEachContext) {
if (children == null) {
return children;
}
var traverseContext = getPooledTraverseContext(
null,
null,
forEachFunc,
forEachContext,
);
traverseAllChildren(children, forEachSingleChild, traverseContext);
releaseTraverseContext(traverseContext);
}
Parameters: children
, forEachFunc
, forEachContext
.
First, we obtain a traversal context object, traverseContext
, via getPooledTraverseContext
, and then call the traverseAllChildren
method to traverse all descendant nodes of the provided children
.
Finally, we release the current 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,
};
}
}
A traverseContextPool
is defined to avoid the cost of recreating new objects every time, with a size of 10. The getPooledTraverseContext
method accepts four parameters: mapResult
, keyPrefix
, mapFunction
, mapContext
.
These are then assigned to traverseContext
, and in addition, a counter property count
is added.
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;
}
It accepts four parameters. First, it checks the type of the children
parameter; if invalid, children
is considered null
.
The subsequent series of checks means that when children
is a single valid React element, the callback
function is executed, and 1
is returned. Since this function will be called recursively later, this also serves as the exit point for the recursive calls.
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 {
When the provided children
is an Array
, this Children Array is iterated, and the current function traverseAllChildrenImpl
is called for each element within it.
It’s important to note the parameters passed: nextName
is used to add a key
value to the React element being traversed, and callback
and traverseContext
have the same values as the current function, ensuring that each child element can also apply the current callback
and access the original 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,
);
}
}
When it’s not an Array
but an iterable object, the traverseAllChildrenImpl
method is called recursively, similar to the above.
For other cases, the child is considered invalid, and an error is reported.
return subtreeCount;
}
Finally, it returns the count of all descendant elements.
Overall, the purpose of the traverseAllChildrenImpl
method is to traverse all descendant elements of the given children
, call the callback
method on each descendant element, and assign a unique key
value within the current context to each element as a parameter to be passed.
Returning to the forEach
method:
traverseAllChildren(children, forEachSingleChild, traverseContext);
This line means traversing all descendant elements of the given children
and calling the forEachSingleChild
method on them.
function forEachSingleChild(bookKeeping, child, name) {
var { func, context } = bookKeeping;
func.call(context, child, bookKeeping.count++);
}
This passed callback
method, forEachSingleChild
, retrieves func
and context
from the bookKeeping
parameter (which is traverseContext
), and then calls func
with context
as its this
context, and child
and the counter count
as arguments. Here, func
is the forEachFunc
parameter of forEachChildren
, which is the function ultimately provided by the end-user.
releaseTraverseContext(traverseContext);
Releasing the current traverseContext
means setting all its properties to null
and placing it back into traverseContextPool
for future use, thereby improving efficiency.
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;
}
Calls func
on the descendant elements of the provided children
and returns a collection of the results of calling 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);
}
First, if a prefix
is provided, it is escaped to be used as the prefix key
for traverseContext
.
Then, a traverseContext
object is obtained. Similar to forEachChildren
, all descendant elements of children
are traversed, and the given callback
function is executed. Finally, traverseContext
is released.
The only difference is this callback
method: 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);
}
}
This method is very similar to forEachSingleChildren
above. It retrieves result
, keyPrefix
, func
, and context
from bookKeeping
.
result
is actually the empty array initially defined in mapChildren
, keyPrefix
is the escapedPrefix
from mapIntoWithKeyPrefixInternal
, and func
and context
are the corresponding parameters of mapChildren
.
First, mappedChild
is defined as the return value of the user-provided mapFunc
function call. Then, it checks if this return value mappedChild
is an Array
.
If it is, the mapIntoWithKeyPrefixInternal
method is called recursively. Otherwise, if it’s not null
and is a valid React element, a cloned element of mappedChild
(with a new key
value composed of keyPrefix
, the user-assigned key
(mappedChild.key
), and the original childkey
) is used as the map result and pushed into result
.
The entire mapChildren
method calls the mapFunc
method on each descendant element of the provided children
, sets a new key
for the returned result, and finally places each executed result mappedChild
into a list to be returned to the user.
countChildren
function countChildren(children, context) {
return traverseAllChildren(children, emptyFunction.thatReturnsNull, null);
}
This is very simple; it just returns the count of all descendant nodes by traversing them.
emptyFunction.thatReturnsNull
This is a function that returns null
.
toArray
function toArray(children) {
var result = [];
mapIntoWithKeyPrefixInternal(
children,
result,
null,
emptyFunction.thatReturnsArgument,
);
return result;
}
If you understand mapIntoWithKeyPrefixInternal
above, then this is also very simple.
emptyFunction.thatReturnsArgument
is a function that returns its first argument.
** mapSingleChildIntoContext **
var mappedChild = func.call(context, child, bookKeeping.count++);
So this line simply returns the child
itself. The result is then placed into result
, and finally, all result
s are returned to the caller.
Summary
ReactChildren
has four APIs, and these four APIs primarily rely on two methods: traverseAllChildrenImpl
and mapSingleChildIntoContext
. Other methods are composite calls built upon these.
Another noteworthy point is the use of the object pool, traverseContextPool
. I believe this is because traverseContext
objects are frequently created due to recursive calls, and repeatedly creating new objects requires reallocating memory in the heap, which is costly. Therefore, an object pool was introduced to improve performance.
Related Articles
- {% post_link react-source-code-analyze-1 React Source Code Analysis - Entry File %}
- {% post_link react-source-code-analyze-2 React Source Code Analysis - ReactBaseClasses %}
- {% post_link react-source-code-analyze-3 React Source Code Analysis - ReactChildren %}
- {% post_link react-source-code-analyze-4 React Source Code Analysis - ReactElement %}
- {% post_link react-source-code-analyze-5 React Source Code Analysis - onlyChildren %}
This article was published on September 21, 2017 and last updated on September 21, 2017, 2937 days ago. The content may be outdated.