文件目录
/packages/react/src/ReactChildren
文档
ReactChildren 主要用于组合模式,详细可以去看看ant-design的Radio.Group
, CheckBox.Group
;
children
this.props.children
其实是一个 ReactElement对象或者是一个数组它的值也是ReactElement
, 查看demo可以看到控制台的输出。
React.Children.map
React.Children.map(this.props.children, (item) => [item, [item, [item]]])
可以看到一个挺有趣的现象,多层嵌套的数组平铺成一维数组,即[item, [item, [item]]]
=> [item, item, item]
, 但可以注意一下各item的key
源码
先看看 mapChildren
/**
* @param {?*} children
* @param {function(*, int)} func 遍历的方式
* @param {*} context 上下文
* @return {object} 遍历完后的结果
*/
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
// 遍历出来的元素会丢到 result 中最后返回出去
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
mapIntoWithKeyPrefixInternal
/**
* @param {?*} children <p>123<p>
* @param {Array} array []
* @param {string} prefix ""
* @param {func} func item => [item, [item, [item]]]
* @param {*} context undefined
*/
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
// 这里是处理 key, 看下面
let escapedPrefix = '';
if (prefix != null) {
escapedPrefix = escapeUserProvidedKey(prefix) + '/';
}
// getPooledTraverseContext 和 releaseTraverseContext 是配套的函数
// 用处其实很简单,就是维护一个大小为 10 的对象重用池
// 每次从这个池子里取一个对象去赋值,用完了就将对象上的属性置空然后丢回池子
const traverseContext = getPooledTraverseContext(
array,
escapedPrefix,
func,
context,
);
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
// 将当前 traverseContext 都的属性置空,然后丢回池子
releaseTraverseContext(traverseContext);
}
escapeUserProvidedKey
eg. ".0/.0"
, ".0/.1:0"
, ".0/.1:1:0"
将 /
匹配,然后在/
加一个/
=> ".0//.0"
,
replace 的 第二个参数 '$&'
表示匹配的内容
const userProvidedKeyEscapeRegex = /\/+/g;
function escapeUserProvidedKey(text) {
return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
}
getPooledTraverseContext 和 releaseTraverseContext
getPooledTraverseContext
和 releaseTraverseContext
是配套使用的,他们主要是维护一个长度为10对象池,getPooledTraverseContext
就是从对象池中拿一个对象出来然后赋值,releaseTraverseContext
就是将 traverseContext 的属性赋值为空,然后重新放会到池子里面。这样做是因为减少 创建对线和释放对象的性能消耗。
/**
* 从池中拿一个对象然后赋值,要是池子没有那直接返回一个对象
* @param {Array} mapResult 遍历后的结果存放
* @param {string} keyPrefix key 值
* @param {func} maoFunction (item) => {}
* @param {*} mapContext undefined
* @returns {result, keyPrefix, func, context, count = 0}
*/
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);
}
}
traverseAllChildren
/**
* @param {*} children <p>123</p> 或者是 [<p>123</p>, <p>456</p>]
* @param callback mapSingleChildIntoContext
* @param {result, keyPrefix, func, context, count = 0} traverseContext
*/
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
traverseAllChildrenImple
/**
* @param {?*} children <p>123</p> 或者是 [<p>123</p>, <p>456</p>]
* @param {!string} nameSoFar 名字路径
* @param {!function} callback mapSingleChildIntoContext
* @param {?*} traverseContext {result, keyPrefix, func, context, count = 0}
* @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;
}
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;
}
}
}
// 如果 children 是可以渲染的节点的话, 比如是<Demo>123</Demo>, 这种情况下,children 是“123”, 就直接调用 callback,
// 如果 children 是 [<p>123</p>, <p></p>] 就跳过往下走
// callback 是 mapSingleChildIntoContext
if (invokeCallback) {
callback(
traverseContext,
children,
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 nextNamePrefix =
nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
// 节点是数组的话,就开始遍历数组,并且把数组中的每个元素再递归执行 traverseAllChildrenImpl
// 如果children 是数据的话,遍历children数组, 然后在对每个元素进行
// mapSingleChildIntoContext
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 {
// 不是数组的话,就看看 children 是否可以支持迭代
// 就是通过 obj[Symbol.iterator] 的方式去取
const iteratorFn = getIteratorFn(children);
// 只有取出来对象是个函数类型才是正确的
if (typeof iteratorFn === 'function') {
// 然后就是执行迭代器,重复上面 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,
);
}
} else if (type === 'object') {
let addendum = '';
const childrenString = '' + children;
}
}
return subtreeCount;
}
- 该函数用于平铺节点
- 我们要记住callback 是
mapSingleChildIntoContext
- 判断
children
类型- 为数字,字符串,还有单个节点的时候直接执行
mapSingleChildIntoContext
- 要是为数组的话遍历数组,再执行
traverseAllChildrenImpl
- 不是数组的话判断一下children 是不是可迭代的,要是是对象的话就抛出对象
- 为数字,字符串,还有单个节点的时候直接执行
mapSingleChildIntoContext
/**
* 这个函数只有当传入的 child 是单个节点是才会调用
* @param bookKeeping traverseContext
* @param child 传入的节点
* @param childKey 节点的 key
*/
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
const {result, keyPrefix, func, context} = bookKeeping;
// func => (item) => [item, [item, [item]]]
let mappedChild = func.call(context, child, bookKeeping.count++);
// 判断函数返回值是否为数组
// mappedChild 的结果是 [item, [item, [item]]]
// 我们说 (item) => [item, [item, [item]]] 会平铺成 [item, item, item]
if (Array.isArray(mappedChild)) {
// 是数组的话就回到最先调用的函数中
// 然后回到之前 traverseAllChildrenImpl 摊平数组的问题
// 假如 c => [item, [item, [item]]],当执行这个函数时,返回值应该是 [item, [item, [item]]]
// 然后 [item, [item, [item]]] 会被当成 children 传入
// traverseAllChildrenImpl 内部逻辑判断是数组又会重新递归执行
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);
}
}
看看流程
eg.
React.Children.map(this.props.children, (item) => [item, [item, [item]]])
- children =>
[<p>123</p>, <p>456</p>]
, func =>(item) => [item, [item, [item]]
, result = []
先进行
mapIntoWithKeyPrefixInternal
, 传入上面三个值, 我们直接忽略key然后从池里赋值
func
和result
,prefix
, 此时traverseContext
result: [], keyPrefix: '', func: (item) => [item, [item, item]], context: undefined count: 0
然后执行
traverseAllChildren
再执行
traverseAllChildrenImpl
, 判断 当前children 是不是可以直接渲染是的话:那直接执行 mapSingleChildIntoContext, 将当前节点放到result
但现在我们的
children
是数组, 所以我们要逐个遍历, 再执行traverseAllChildrenImpl
eg.此时我们拿到
children[0]
, 然后执行traverseAllChildrenImpl
,然后进入mapSingleChildIntoContext,
// mapSingleChildIntoContext // bookKeeping:{ result: [], keyPrefix: "", func: (item) => [item, [item, [item]]] } // child: <p>123</p>
在
mapSingleChildIntoContext
内执行了func, 所以得到的结果是mappedChild = [item, [item, [item]]]
, 然后判断mappedChild是否是数组,是的话把 mappedChild 当成children 从第一步开始, 但递归后此时func是c => c
,从
traverseAllChildrenImpl
判断mappedChild
是数组,然后遍历他,拿出第一个item,再执行了一次traverseAllChildrenImpl
,然后是ReactElement,就执行了mapSingleChildIntoContext
, 但注意此时的func 是c => c
, 判定结果不是数组,将结果push 进result,mapSingleChildIntoContext结束
。回到traverseAllChildrenImpl
, 然后执行mappedChild[1]
, 又在traverseAllChildrenImpl
方法判定是数组,再取mappedChild[1][0]
去做mapSingleChildIntoContext
, 然后执行 funcc => c
,再push进result,mapSingleChildIntoContext结束
, 再一次回到traverseAllChildrenImpl
,直到mapChildren
遍历完。
以上如此类推