React.children.map 使用
class App enrends Reacr.Component{
return (
<div>
{React.children.map(this.props.children, item => [item,item])}
</div>
)
}
原理解析
入口方法
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
入口方法 mapChildren 会进行 children 判断,然后决定是否进行后续操作。
核心方法 mapIntoWithKeyPrefixInternal。
参数列表
- children: 需要处理的子组件列表
- result: 最终处理的结果
- null:这个参数不用关心,类似于一个组件 id 的前缀,防止 reactid 重复用的
- func:用户自定义的对每个 children 的处理函数
- context:处理 children 的上下文
mapIntoWithKeyPrefixInternal 方法
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); // 开始处理 children
releaseTraverseContext(traverseContext); // 处理完毕后将 traverseContext 返还给对象池
}
这里面出现了很多新的方法,但是其核心是 traverseAllChildren, 它是真正调用 func 处理 children 并将最终结果返回。
getPooledTraverseContext 方法可以理解为 React 建立的一个对象池,这个方法就是从对象池中获取一个对象。
releaseTraverseContext 方法是将 getPooledTraverseContext 获取的对象返还给对象池。
对象池
const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext(
mapResult,
keyPrefix,
mapFunction,
mapContext,
) {
if (traverseContextPool.length) { // 如果对象池中有已经存在的对象,则直接此次处理的 children 数组的基本信息记录在 traverseContext 对象中
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);
}
}
上述的两个方法,getPooledTraverseContext是从对象池中获取一个对象,releaseTraverseContext 是将使用的对象再放回对象池中,但是看到 pop 和 push 方法会感到疑惑。因为感觉上这里不管什么时候得到的都是一个对象。这里先留个悬念,后面再做解释。
traverseAllChildren 开始解析 children
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
其实这个方法只是做了简单的判断,然后就会调用其核心方法 traverseAllChildrenImpl。当然,他的参数列表中有一个 mapSingleChildIntoContext 方法,这个就是 callback,这个方法后续再讲解。
traverseAllChildrenImpl
function traverseAllChildrenImpl(
children,
nameSoFar,
callback,
traverseContext,
) {
const type = typeof children;
// 从这里看出,children 不能是 boolean 和 undefined
if (type === 'undefined' || type === 'boolean') {
// All of the above are perceived as null.
children = null;
}
let invokeCallback = false;
if (children === null) {
invokeCallback = true;
} else {
switch (type) {
case 'string':
case 'number':
invokeCallback = true;
break;
case 'object':
switch (children.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = true;
}
}
}
if (invokeCallback) {
callback( // 所有子组件最终解析归宿,都会走进这个分支 mapSingleChildIntoContext === 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;
}
let child;
let nextName;
let subtreeCount = 0; // Count of children found in the current subtree.
const nextNamePrefix =
nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getComponentKey(child, i);
subtreeCount += traverseAllChildrenImpl(
child,
nextName,
callback,
traverseContext,
);
}
} else {
const iteratorFn = getIteratorFn(children);
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.',
);
didWarnAboutMaps = true;
}
}
const iterator = iteratorFn.call(children);
let step;
let 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') {
let addendum = '';
if (__DEV__) {
addendum =
' If you meant to render a collection of children, use an array ' +
'instead.' +
ReactDebugCurrentFrame.getStackAddendum();
}
const 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,
);
}
}
return subtreeCount;
}
从这个方法中你可以得出那些类型是合法的 React.children ,而那些子元素是不合法的,以及 react 在 DEV 阶段的一些处理。
这个方法最重要的是对子元素类型的处理,在这个处理过程中,通过判断子元素的类型来进行相应的处理,当子元素是 null、string、number、REACT_ELEMENT_TYPE、REACT_PORTAL_TYPE 这是就可以判断出,这个是单个的子元素,直接调用 callback === mapSingleChildIntoContext 方法进行处理。如果不是上述类型,则会进行进一步处理,但是处理的基本思路是,使用 traverseAllChildrenImpl 方法对 children 进行递归处理,直到每一个 children 被 mapSingleChildIntoContext 接收处理。
mapSingleChildIntoContext
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
const {result, keyPrefix, func, context} = bookKeeping;
let mappedChild = func.call(context, child, bookKeeping.count++);
if (Array.isArray(mappedChild)) { // 如果执行用户自定义的回调返回值还是数组的话,就再次将数组解析,直到 children 是单个的 react 合法元素
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);
}
}
可以注意到,当给出的 children 是数组时,会调用 mapIntoWithKeyPrefixInternal,这时如果对象池没有可用对象就会新创建并且在使用后将其回收。这就解释了为什么对对象池进行 pop 和 push 操作时,获取的对象可能不是同一个的问题。
总结
总体来说,react 使用 map 处理子元素是,有以下特点:
- 当用户的自定义处理函数返回值是数组时,react 会接着循环这个数组,直到每个子元素都不是数组为止。也就是所谓的对
children进行降维打击。 - 整体来说,
map的整个处理过程流程很清晰,采用了 对象池的思路,减少对象的创建,以避免不必要的性能消耗和内存抖动的问题。 - 看源码的过程需要一点耐心,不要对某个方法的实现死扣,要先了解大致流程以后,再对其实现代码进行解析,这样子记忆的会更加清晰。