Skip to content

Commit

Permalink
Refactor React.Children to reduce indirection (#18332)
Browse files Browse the repository at this point in the history
* Don't pool traversal context

* Remove traverseAllChildrenImpl indirection

All usages are internal so we can simply use the inner function directly.

* Implement forEach through map

* Remove second usage of traverseAllChildren

This isn't useful by itself but makes the layering easier to follow. traverseAllChildren is only used at the lowest layer now.

* Reimplement count() and toArray() in terms of map()

* Inline the only use of mapSingleChildIntoContext

* Move forEach down in the file

* Use the language

Get rid of the traversal context. Use closures.

* Make mapIntoArray take an already escaped prefix

* Move count state out of mapIntoArray

* Inline traverseAllChildren into mapIntoArray

* Inline handleChild into mapIntoArray
  • Loading branch information
gaearon committed Mar 23, 2020
1 parent fd61f7e commit fc96a52
Showing 1 changed file with 91 additions and 180 deletions.
271 changes: 91 additions & 180 deletions packages/react/src/ReactChildren.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,58 +50,29 @@ function escapeUserProvidedKey(text) {
return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
}

const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext(
mapResult,
keyPrefix,
mapFunction,
mapContext,
) {
if (traverseContextPool.length) {
const 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,
};
}
}

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);
/**
* Generate a key string that identifies a component within a set.
*
* @param {*} component A component that could contain a manual key.
* @param {number} index Index that is used if a manual key is not provided.
* @return {string}
*/
function getComponentKey(component, index) {
// Do some typechecking here since we call this blindly. We want to ensure
// that we don't block potential future ES APIs.
if (
typeof component === 'object' &&
component !== null &&
component.key != null
) {
// Explicit key
return escape(component.key);
}
// Implicit key determined by the index in the set
return index.toString(36);
}

/**
* @param {?*} children Children tree container.
* @param {!string} nameSoFar Name of the key path so far.
* @param {!function} callback Callback to invoke with each child found.
* @param {?*} traverseContext Used to pass information throughout the traversal
* process.
* @return {!number} The number of children in this subtree.
*/
function traverseAllChildrenImpl(
children,
nameSoFar,
callback,
traverseContext,
) {
function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
const type = typeof children;

if (type === 'undefined' || type === 'boolean') {
Expand Down Expand Up @@ -129,13 +100,33 @@ function traverseAllChildrenImpl(
}

if (invokeCallback) {
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,
);
const child = children;
let mappedChild = callback(child);
// 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:
let childKey =
nameSoFar === '' ? SEPARATOR + getComponentKey(child, 0) : nameSoFar;
if (Array.isArray(mappedChild)) {
let escapedChildKey = '';
if (childKey != null) {
escapedChildKey = escapeUserProvidedKey(childKey) + '/';
}
mapIntoArray(mappedChild, array, escapedChildKey, c => c);
} else if (mappedChild != null) {
if (isValidElement(mappedChild)) {
mappedChild = cloneAndReplaceKey(
mappedChild,
// Keep both the (mapped) and old keys if they differ, just as
// traverseAllChildren used to do for objects as children
escapedPrefix +
(mappedChild.key && (!child || child.key !== mappedChild.key)
? escapeUserProvidedKey(mappedChild.key) + '/'
: '') +
childKey,
);
}
array.push(mappedChild);
}
return 1;
}

Expand All @@ -149,11 +140,12 @@ function traverseAllChildrenImpl(
for (let i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getComponentKey(child, i);
subtreeCount += traverseAllChildrenImpl(
subtreeCount += mapIntoArray(
child,
array,
escapedPrefix,
nextName,
callback,
traverseContext,
);
}
} else {
Expand Down Expand Up @@ -188,11 +180,12 @@ function traverseAllChildrenImpl(
while (!(step = iterator.next()).done) {
child = step.value;
nextName = nextNamePrefix + getComponentKey(child, ii++);
subtreeCount += traverseAllChildrenImpl(
subtreeCount += mapIntoArray(
child,
array,
escapedPrefix,
nextName,
callback,
traverseContext,
);
}
} else if (type === 'object') {
Expand All @@ -218,121 +211,6 @@ function traverseAllChildrenImpl(
return subtreeCount;
}

/**
* Traverses children that are typically specified as `props.children`, but
* might also be specified through attributes:
*
* - `traverseAllChildren(this.props.children, ...)`
* - `traverseAllChildren(this.props.leftPanelChildren, ...)`
*
* The `traverseContext` is an optional argument that is passed through the
* entire traversal. It can be used to store accumulations or anything else that
* the callback might find relevant.
*
* @param {?*} children Children tree object.
* @param {!function} callback To invoke upon traversing each child.
* @param {?*} traverseContext Context for traversal.
* @return {!number} The number of children in this subtree.
*/
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}

return traverseAllChildrenImpl(children, '', callback, traverseContext);
}

/**
* Generate a key string that identifies a component within a set.
*
* @param {*} component A component that could contain a manual key.
* @param {number} index Index that is used if a manual key is not provided.
* @return {string}
*/
function getComponentKey(component, index) {
// Do some typechecking here since we call this blindly. We want to ensure
// that we don't block potential future ES APIs.
if (
typeof component === 'object' &&
component !== null &&
component.key != null
) {
// Explicit key
return escape(component.key);
}
// Implicit key determined by the index in the set
return index.toString(36);
}

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

/**
* Iterates through children that are typically specified as `props.children`.
*
* See https://reactjs.org/docs/react-api.html#reactchildrenforeach
*
* The provided forEachFunc(child, index) will be called for each
* leaf child.
*
* @param {?*} children Children tree container.
* @param {function(*, int)} forEachFunc
* @param {*} forEachContext Context for forEachContext.
*/
function forEachChildren(children, forEachFunc, forEachContext) {
if (children == null) {
return children;
}
const traverseContext = getPooledTraverseContext(
null,
null,
forEachFunc,
forEachContext,
);
traverseAllChildren(children, forEachSingleChild, traverseContext);
releaseTraverseContext(traverseContext);
}

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

let mappedChild = func.call(context, child, bookKeeping.count++);
if (Array.isArray(mappedChild)) {
mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
} else if (mappedChild != null) {
if (isValidElement(mappedChild)) {
mappedChild = 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);
}
}

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

/**
* Maps children that are typically specified as `props.children`.
*
Expand All @@ -351,7 +229,17 @@ function mapChildren(children, func, context) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
let count = 0;
mapIntoArray(
children,
result,
'',
'',
function(child) {
return func.call(context, child, count++);
},
context,
);
return result;
}

Expand All @@ -365,7 +253,32 @@ function mapChildren(children, func, context) {
* @return {number} The number of children.
*/
function countChildren(children) {
return traverseAllChildren(children, () => null, null);
let n = 0;
mapChildren(children, () => n++);
return n;
}

/**
* Iterates through children that are typically specified as `props.children`.
*
* See https://reactjs.org/docs/react-api.html#reactchildrenforeach
*
* The provided forEachFunc(child, index) will be called for each
* leaf child.
*
* @param {?*} children Children tree container.
* @param {function(*, int)} forEachFunc
* @param {*} forEachContext Context for forEachContext.
*/
function forEachChildren(children, forEachFunc, forEachContext) {
mapChildren(
children,
function() {
forEachFunc.apply(this, arguments);
// Don't return anything.
},
forEachContext,
);
}

/**
Expand All @@ -375,9 +288,7 @@ function countChildren(children) {
* See https://reactjs.org/docs/react-api.html#reactchildrentoarray
*/
function toArray(children) {
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, child => child);
return result;
return mapChildren(children, child => child) || [];
}

/**
Expand Down

0 comments on commit fc96a52

Please sign in to comment.