本文基于 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, // mapFunc
context, // 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
// 是可渲染的节点则为 true
let invokeCallback = false;
// 判断是否调用,children === null、type 为可渲染的节点则 invokeCallback 为 true
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;
}
}
}
// 如果 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 c2
if (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 一遍并且替换掉 key
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);
}
}
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 => {
debugger
return 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
是最复杂的,把每个函数的意义和签名都读懂之后我对整体有了比较深的认识。看一看流程图,整个过程就清楚了。
广告时间