
本文基于 React V16.8.6
,本文代码地址
- 测试代码
- 源码讲解
React 中一个元素可能有 0 个、1 个或者多个直接子元素,React 导出的 Children 中包含 5 个处理子元素的方法。
map
类似array.map
forEach
类似array.forEach
count
类似array.length
toArray
only
React 内部处理 Children 的几个重要函数包括
mapChildren
traverseAllChildrenImpl
mapIntoWithKeyPrefixInternal
mapSingleChildIntoContext
getPooledTraverseContext
releaseTraverseContext
源码都在 packages/react/src/ReactChildren.js
中。
导出的语句
export {forEachChildren as forEach,mapChildren as map,countChildren as count,onlyChild as only,toArray,
};
Children API
map
类似 array.map
,但有一下几个不同点:
- 返回的结果一定是一个一维数组,多维数组会被自动摊平
- 对返回的每个节点,如果
isValidElement(el) === true
,则会给它加上一个 key,如果元素本来就有 key,则会重新生成一个新的 key
map
的用法:第一个参数是要遍历的 children,第二个参数是遍历的函数,第三个是 context,执行遍历函数时的 this
。
如果 children == null
,则直接返回了。
mapChildren
/*** Maps children that are typically specified as `props.children`.* 用来遍历 `props.children`** @param {?*} children Children tree container.* @param {function(*, int)} func The map function.* @param {*} context Context for mapFunction.* @return {object} Object containing the ordered map of results.*/
function mapChildren(children, func, context) {if (children == null) {return children;}// 遍历出来的元素会丢到 result 中最后返回出去const result = [];mapIntoWithKeyPrefixInternal(children, result, null, func, context);return result;
}
mapIntoWithKeyPrefixInternal
将 children 完全遍历,遍历的节点最终全部存到 array 中,是 ReactElement 的节点会更改 key 之后再放到 array 中。
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {// 这里是处理 key,不关心也没事let escapedPrefix = '';if (prefix != null) {escapedPrefix = escapeUserProvidedKey(prefix) + '/';}// getPooledTraverseContext 和 releaseTraverseContext 是配套的函数// 用处其实很简单,就是维护一个大小为 10 的对象重用池// 每次从这个池子里取一个对象去赋值,用完了就将对象上的属性置空然后丢回池子// 维护这个池子的用意就是提高性能,毕竟频繁创建销毁一个有很多属性的对象消耗性能const traverseContext = getPooledTraverseContext(array, // result escapedPrefix, // ''func, // mapFunccontext, // context);// 最核心的一句traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);releaseTraverseContext(traverseContext);
}
getPooledTraverseContext
getPooledTraverseContext
和 releaseTraverseContext
,这两个函数是用来维护一个对象池,池子最大为10。Children 需要频繁的创建对象会导致性能问题,所以维护一个固定数量的对象池,每次从对象池拿一个对象进行复制,使用完将各个属性 reset。

const POOL_SIZE = 10;
const traverseContextPool = [];
// 返回一个传入参数构成的对象
// traverseContextPool 长度为 0 则自己构造一个对象出来,否则从 traverseContextPool pop 一个对象
// 再对这个对象的各个属性进行赋值
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,};}
}
releaseTraverseContext

将 getPooledTraverseContext
产生的对象加入数组中,对象池 >= 10 则不用管
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);}
}
traverseAllChildren
没太多好说的
function traverseAllChildren(children, callback, traverseContext) {if (children == null) {return 0;}return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
traverseAllChildrenImpl
它的作用可以理解为
children
是可渲染节点,则调用mapSingleChildIntoContext
把 children 推入 result 数组中children
是数组,则再次对数组中的每个元素调用traverseAllChildrenImpl
,传入的 key 是最新拼接好的children
是对象,则通过children[Symbol.iterator]
获取到对象的迭代器iterator
, 将迭代的结果放到traverseAllChildrenImpl
处理
函数核心作用就是通过把传入的 children 数组通过遍历摊平成单个节点,然后去执行 mapSingleChildIntoContext
。
这个函数比较复杂,函数签名是这样的
children
要处理的 childrennameSoFar
父级 key,会一层一层拼接传递,用 : 分隔callback
如果当前层级是可渲染节点,undefined
、boolean
会变成null
,string
、number
、$$typeof
是REACT_ELEMENT_TYPE
或者REACT_PORTAL_TYPE
,会调用mapSingleChildIntoContext
处理traverseContext
对象池中拿出来的一个对象
/*** @param {?*} children Children tree container. `Children.map` 的第一个参数,要处理的 children* @param {!string} nameSoFar Name of the key path so far.* @param {!function} callback Callback to invoke with each child found. map 时 callback 是* `mapSingleChildIntoContext`* @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,
) {// 这个函数核心作用就是通过把传入的 children 数组通过遍历摊平成单个节点// 然后去执行 mapSingleChildIntoContext// 开始判断 children 的类型const type = typeof children;if (type === 'undefined' || type === 'boolean') {// All of the above are perceived as null.children = null;}// 决定是否调用 callback// 是可渲染的节点则为 truelet invokeCallback = false;// 判断是否调用,children === null、type 为可渲染的节点则 invokeCallback 为 trueif (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;}}}// 如果 children 是可以渲染的节点的话,就直接调用 callback// callback 是 mapSingleChildIntoContext// 我们先去阅读下 mapSingleChildIntoContext 函数的源码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.// const SEPARATOR = '.';nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,);return 1;}// nextName 和 nextNamePrefix 都是在处理 key 的命名let child;let nextName;let subtreeCount = 0; // Count of children found in the current subtree.// const SUBSEPARATOR = ':';const nextNamePrefix =nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;// 节点是数组的话,就开始遍历数组,并且把数组中的每个元素再递归执行 traverseAllChildrenImpl// 这一步操作也用来摊平数组的// React.Children.map(this.props.children, c => [[c, c]])// c => [[c, c]] 会被摊平为 [c, c, c, c]// 这里如果看不明白的话过会在 mapSingleChildIntoContext 中肯定能看明白if (Array.isArray(children)) {for (let i = 0; i < children.length; i++) {child = children[i];nextName = nextNamePrefix + getComponentKey(child, i); // .$dasdsa:subtreeCount += traverseAllChildrenImpl(child,nextName, // 不同点是 nameSoFar 变了,它会在每一层不断拼接,用 : 分隔callback,traverseContext,);}} else {// 不是数组的话,就看看 children 是否可以支持迭代// 通过 obj[Symbol.iterator] 的方式去取const iteratorFn = getIteratorFn(children);// ... 中间有部分 __DEV__ 下检测使用正确性的代码// 只有取出来对象是个函数类型才是正确的// 然后就是执行迭代器,重复上面 if 中的逻辑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,);}}return subtreeCount;
}
mapSingleChildIntoContext
将 child
推入 traverseContext
的 result 数组中,child
如果是 ReactElement,则更改 key 了再推入。
只有当传入的 child 是可渲染节点才会调用。如果执行了 mapFunc 返回的是一个数组,则会将数组放到 mapIntoWithKeyPrefixInternal
继续处理。
/*** @param bookKeeping 就是我们从对象池子里取出来的东西,`traverseContext`* @param child 传入的节点,`children`* @param childKey 节点的 key,`nameSoFar`*/
function mapSingleChildIntoContext(bookKeeping, child, childKey) {const {result, keyPrefix, func, context} = bookKeeping; // traverseContext// func 就是我们在 React.Children.map(this.props.children, c => c)// 中传入的第二个函数参数let mappedChild = func.call(context, child, bookKeeping.count++);// 判断函数返回值是否为数组// 因为可能会出现这种情况// React.Children.map(this.props.children, c => [c, c])// 对于 c => [c, c] 这种情况来说,每个子元素都会被返回出去两次// 也就是说假如有 2 个子元素 c1 c2,那么通过调用 React.Children.map(this.props.children, c => [c, c]) 后// 返回的应该是 4 个子元素,c1 c1 c2 c2if (Array.isArray(mappedChild)) {// 是数组的话就回到最先调用的函数中// 然后回到之前 traverseAllChildrenImpl 摊平数组的问题// 假如 c => [[c, c]],当执行这个函数时,返回值应该是 [c, c]// 然后 [c, c] 会被当成 children 传入// traverseAllChildrenImpl 内部逻辑判断是数组又会重新递归执行// 所以说即使你的函数是 c => [[[[c, c]]]]// 最后也会被递归摊平到 [c, c, c, c]mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);} else if (mappedChild != null) {// 不是数组且返回值不为空,判断返回值是否为有效的 Element// 是的话就把这个元素 clone 一遍并且替换掉 keyif (isValidElement(mappedChild)) {mappedChild = cloneAndReplaceKey(mappedChild,// Keep both the (mapped) and old keys if they differ, just as // traverseAllChildren used to do for objects as childrenkeyPrefix +(mappedChild.key && (!child || child.key !== mappedChild.key)? escapeUserProvidedKey(mappedChild.key) + '/': '') +childKey,);}result.push(mappedChild);}
}
map 测试
map
代码就是上面这些,写一个 demo 看看执行过程。
class Child extends Component {render() {console.log('this.props.children', this.props.children)const c = React.Children.map(this.props.children, c => {debuggerreturn c})console.log('mappedChildren', c)return <div>{c}</div>}
}export default class Children extends Component {render() {// return 的代码包含 2 种情况:children 是和不是数组return (<Child><div>childrendasddadas<div>childrendasddadas</div><div>childrendasddadas</div></div><div key="key2">childrendasddadas</div><div key="key3">childrendasddadas</div>{[<div key="key4">childrendasddadas</div>,<div key="key5=">childrendasddadas</div>,<div key="key6:">childrendasddadas</div>,]}</Child>)}
}
打印的结果如下

React.Children.map
就是把传进去的 this.props.children
全部摊平,最后返回的一定是一维数组,数组中的对象都会添加上 key 属性。对 mappedChildren
key 的生成做分析如下。
this.props.children
自身是一个数组,在第一次调用 traverseAllChildrenImpl
时,nextName
为 .0
,第一个 child 执行 traverseAllChildrenImpl
时,invokeCallback
为 true,nameSoFar
为 .0
,再执行 mapSingleChildIntoContext
走到 cloneAndReplaceKey
,新 key 生成为 .0
(因为 (mappedChild.key && (!child || child.key !== mappedChild.key)
为 false,keyPrefix
为空字符串)。
第二个和第三个 child 的 key 加上了 .$
,在 traverseAllChildrenImpl
中,遍历到第二个和第三个下标时 nextName = nextNamePrefix + getComponentKey(child, i);
,nextNamePrefix
是 .
,i
是 2、3,getComponentKey
执行,由于它有自己的 key,所以 escape 后变成 .
+ $key2
=> .$key2
,.$key3
同理。
function escape(key) {const escapeRegex = /[=:]/g;const escaperLookup = {'=': '=0', // 替换 =':': '=2', // 替换 :};const escapedString = ('' + key).replace(escapeRegex, function(match) {return escaperLookup[match];});return '$' + escapedString; // 返回的字符串前面加上 $
}
第四、五、六个是嵌套在数组里面的,同上面,this.props.children
遍历到这个数组的时候索引为 3。传给下一轮 traverseAllChildrenImpl
的 nameSoFar
为 .3
、child
为数组,下一 轮traverseAllChildrenImpl
,children 是一个数组,对其进行遍历,nextNamePrefix
是 .3:
,由下面这句计算出来。
const nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
而 getComponentKey(child, i)
,由于数组中的每个元素有自己的 key,所以返回的是 $key4
、 $key5=0
和 $key6=2
,拼接出来就是 .3:$key4
、.3:$key5=0
、.3:$key6=2
,这里第五、六个的 =
和 :
被 escape 处理成了 =0
和 =2
。
上面例子代码 debugger 时的调用栈:

下面贴一张 map
的流程图。

forEach
类似 array.forEach
。
和 map
的不同之处是传给 getPooledTraverseContext
的参数 result
为 null,因为 forEach 只需要遍历,不需要返回一个数组。另外 traverseAllChildren
它的第二个参数变成了 forEachSingleChild
。
它没有 map
那么复杂。
forEachChildren
调用 traverseAllChildren
让每个 child 都被放到 forEachSingleChild
中执行
/*** Iterates through children that are typically specified as `props.children`.* The provided forEachFunc(child, index) will be called for each* leaf child.** @param {?*} children Children tree container. `this.props.children`* @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);
}
forEachSingleChild
把 children
中的每个元素放到 func
中执行
/*** 把 `children` 中的每个元素放到 `func` 中执行** @param bookKeeping traverseContext* @param child 单个可 render child* @param name 这里没有用到*/
function forEachSingleChild(bookKeeping, child, name) {const {func, context} = bookKeeping;func.call(context, child, bookKeeping.count++);
}
count
计算 children 的个数,计算的是摊平后数组元素的个数
countChildren
traverseAllChildren
有一个返回值 subtreeCount
,表示子节点的个数,traverseAllChildren
遍历所有 child 之后,subtreeCount
会统计出结果。
/*** 计算 children 的个数,计算的是摊平后数组元素的个数* Count the number of children that are typically specified as* `props.children`.** @param {?*} children Children tree container.* @return {number} The number of children.*/
function countChildren(children) {return traverseAllChildren(children, () => null, null);
}
toArray
同 mapChildren(children, child => child, context)
/*** 是 `mapChildren(children, child => child, context)` 版本* Flatten a children object (typically specified as `props.children`) and* return an array with appropriately re-keyed children.*/
function toArray(children) {const result = [];mapIntoWithKeyPrefixInternal(children, result, null, child => child);return result;
}
only
如果参数是一个 ReactElement
,则直接返回它,否则报错,用在测试中,正式代码没什么用。
/*** Returns the first child in a collection of children and verifies that there* is only one child in the collection.* The current implementation of this function assumes that a single child gets* passed without a wrapper, but the purpose of this helper function is to* abstract away the particular structure of children.** @param {?object} children Child collection structure.* @return {ReactElement} The first and only `ReactElement` contained in the* structure.*/
function onlyChild(children) {invariant(isValidElement(children),'React.Children.only expected to receive a single React element child.',);return children;
}function isValidElement(object) {return (typeof object === 'object' &&object !== null &&object.$$typeof === REACT_ELEMENT_TYPE);
}
导出的函数中,map
是最复杂的,把每个函数的意义和签名都读懂之后我对整体有了比较深的认识。看一看流程图,整个过程就清楚了。