一、例子
function ChildrenDemo(props) {console.log(props.children, 'children30');
console.log(React.Children.map(props.children, item => [item, [item, [item]]]), 'children31');
// console.log(React.Children.map(props.children,item=>item),'children31')
return props.children;
}
export default ()=>(
<ChildrenDemo>
<span key={'.0/'}>1</span>
<span>2</span>
</ChildrenDemo>
)
props.children:
React.Children.map(props.children, item => [item, [item, [item]]]:
看到一个有趣的现象,就是多层嵌套的数组 [item, [item, [item]]]
经过 map()
后,平铺成 [item,item,item]
了,接下来以该例解析React.Child.map()
二、React.Children.map()
作用:
https://zh-hans.reactjs.org/docs/react-api.html#reactchildren
源码:
// React.Children.map(props.children,item=>[item,[item,] ])
function mapChildren(children, func, context) {if (children == null) {return children;}
const result = [];
// 进行基本的判断和初始化后,调用该方法
//props.children,[],null,(item)=>{return [item,[item,] ]},undefined
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
export {
//as 就是重命名了,map 即 mapChildren
forEachChildren as forEach,
mapChildren as map,
countChildren as count,
onlyChild as only,
toArray,
};
解析:
注意 result
,该数组在里面滚了一圈后,会return
结果
三、mapIntoWithKeyPrefixInternal()
作用:getPooledTraverseContext()
/traverseAllChildren()
/releaseTraverseContext()
的包裹器
源码:
// 第一次:props.children , [] , null , (item)=>{return [item,[item,] ]} , undefined
// 第二次:[item,[item,] ] , [] , .0 , c => c , undefined
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
let escapedPrefix = '';
// 如果字符串中有连续多个 / 的话,在匹配的字串后再加 /
if (prefix != null) {escapedPrefix = escapeUserProvidedKey(prefix) + '/';
}
// 从 pool 中找一个对象
//[],'',(item)=>{return [item,[item,] ]},undefined
//traverseContext=
// {// result:[],
// keyPrefix:'',
// func:(item)=>{return [item,[item,] ]},
// context:undefined,
// count:0,
// }
const traverseContext = getPooledTraverseContext(
array,
escapedPrefix,
func,
context,
);
// 将嵌套的数组展平
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
releaseTraverseContext(traverseContext);
}
解析:
① escapeUserProvidedKey()
这个函数一般是第二层递归时,会用到
作用:
在 /
后再加一个/
源码:
const userProvidedKeyEscapeRegex = /\/+/g;
function escapeUserProvidedKey(text) {
// 如果字符串中有连续多个 / 的话,在匹配的字串后再加 /
return (''+ text).replace(userProvidedKeyEscapeRegex,'$&/');
}
解析:
react 对 key 定义的一个规则:
如果字符串中有连续多个 /
的话,在匹配的字串后再加/
例:
let a='aa/a/'
console.log(a.replace(/\/+/g, '$&/')); // aa//a//
② getPooledTraverseContext()
作用:
创建一个对象池,复用Object
,从而减少很多对象创建带来的内存占用和gc
(垃圾回收)的损耗
源码:
// 对象池的最大容量为 10
const POOL_SIZE = 10;
// 对象池
const traverseContextPool = [];
//[],'',(item)=>{return [item,[item,] ]},undefined
function getPooledTraverseContext(
mapResult,
keyPrefix,
mapFunction,
mapContext,
) {
// 如果对象池内存在对象,则出队一个对象,// 并将 arguments 的值赋给对象属性
// 最后返回该对象
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 {
//{// result:[],
// keyPrefix:'',
// func:(item)=>{return [item,[item,] ]},
// context:undefined,
// count:0,
// }
return {
result: mapResult,
keyPrefix: keyPrefix,
func: mapFunction,
context: mapContext,
count: 0,
};
}
}
解析:
在每次 map()
的过程中,每次递归都会用到 traverseContext
对象,
创建 traverseContextPool
对象池的 目的 ,就是 ** 复用里面的对象,
以减少内存消耗 **,并且在 map()
结束时,
将复用的对象初始化,并 push
进对象池中(releaseTraverseContext
),以供下次 map()
时使用
③ mapSingleChildIntoContext()
mapSingleChildIntoContext
是 traverseAllChildren(children, mapSingleChildIntoContext, traverseContext)
的第二个参数,为避免讲 traverseAllChildren
要调头看这个 API,就先分析下
作用:
递归仍是数组的 child
;
将单个 ReactElement
的child
加入 result
中
源码:
//bookKeeping:traverseContext=
// {// result:[],
// keyPrefix:'',
// func:(item)=>{return [item,[item,] ]},
// context:undefined,
// count:0,
// }
//child:<span>1<span/>
//childKey:.0
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
// 解构赋值
const {result, keyPrefix, func, context} = bookKeeping;
//func:(item)=>{return [item,[item,] ]},
//item 即 <span>1<span/>
// 第二个参数 bookKeeping.count++ 很有意思,压根儿没用到,但仍起到计数的作用
let mappedChild = func.call(context, child, bookKeeping.count++);
// 如果根据 React.Children.map()第二个参数 callback,执行仍是一个数组的话,// 递归调用 mapIntoWithKeyPrefixInternal,继续之前的步骤,// 直到是单个 ReactElement
if (Array.isArray(mappedChild)) {//mappedChild:[item,[item,] ]
//result:[]
//childKey:.0
//func:c => c
mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
}
// 当 mappedChild 是单个 ReactElement 并且不为 null 的时候
else if (mappedChild != null) {if (isValidElement(mappedChild)) {
// 赋给新对象除 key 外同样的属性,替换 key 属性
mappedChild = cloneAndReplaceKey(
mappedChild,
// Keep both the (mapped) and old keys if they differ, just as
// traverseAllChildren used to do for objects as children
// 如果新老 keys 是不一样的话,两者都保留,像 traverseAllChildren 对待 objects 做的那样
keyPrefix +
(mappedChild.key && (!child || child.key !== mappedChild.key)
? escapeUserProvidedKey(mappedChild.key) + '/'
: '') +
childKey,
);
}
//result 即 map 时,return 的 result
result.push(mappedChild);
}
}
解析:
(1)让 child
调用 func
方法,所得的结果如果是数组的话继续递归;如果是单个 ReactElement
的话,将其放入 result
数组中
(2)cloneAndReplaceKey()
字如其名,就是赋给新对象除 key
外同样的属性,替换 key
属性
简单看下源码:
export function cloneAndReplaceKey(oldElement, newKey) {
const newElement = ReactElement(
oldElement.type,
newKey,
oldElement.ref,
oldElement._self,
oldElement._source,
oldElement._owner,
oldElement.props,
);
return newElement;
}
(3)isValidElement()
判断是否为 ReactElement
简单看下源码:
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
④ traverseAllChildren()
作用:traverseAllChildrenImpl
的触发器
源码:
// children, mapSingleChildIntoContext, traverseContext
function traverseAllChildren(children, callback, traverseContext) {if (children == null) {return 0;}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
⑤ traverseAllChildrenImpl()
作用:
核心递归函数,目的是展平嵌套数组
源码:
// children, '', mapSingleChildIntoContext, traverseContext
function traverseAllChildrenImpl(
children,
nameSoFar,
callback,
//traverseContext=
// {// result:[],
// keyPrefix:'',
// func:(item)=>{return [item,[item,] ]},
// context:undefined,
// count:0,
// }
traverseContext,
) {
const type = typeof children;
if (type === 'undefined' || type === 'boolean') {
// 以上所有的被认为是 null
// All of the above are perceived as null.
children = null;
}
// 调用 func 的 flag
let invokeCallback = false;
if (children === null) {invokeCallback = true;} else {switch (type) {
case 'string':
case 'number':
invokeCallback = true;
break;
case 'object':
// 如果 props.children 是单个 ReactElement/PortalElement 的话
// 递归 traverseAllChildrenImpl 时,<span>1<span/> 和 <span>2<span/> 作为 child
// 必会触发 invokeCallback=true
switch (children.$$typeof) {
case REACT_ELEMENT_TYPE:
case REACT_PORTAL_TYPE:
invokeCallback = true;
}
}
}
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.
//.$=0
//<span>1<span/> key='.0'
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++) {
//<span>1</span>
child = children[i];
// 不手动设置 key 的话第一层第一个是.0,第二个是.1
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,
);
}
}
// 如果是一个纯对象的话,throw error
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;
}
解析:
分为两部分:
(1)children
是 Object
,并且$$typeof
是REACT_ELEMENT_TYPE
/REACT_PORTAL_TYPE
调用 callback
即mapSingleChildIntoContext
,复制除 key
外的属性,替换 key
属性,将其放入到 result
中
(2)children
是 Array
循环 children
,再用traverseAllChildrenImpl
执行child
三、流程图
四、根据 React.Children.map()的算法出一道面试题
数组扁平化处理:
实现一个 flatten
方法,使得输入一个数组,该数组里面的元素也可以是数组,该方法会输出一个扁平化的数组
// Example
let givenArr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
let outputArr = [1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10]
// 实现 flatten 方法使得 flatten(givenArr)——>outputArr
解法一:根据上面的流程图使用递归
function flatten(arr){var res = [];
for(var i=0;i<arr.length;i++){if(Array.isArray(arr[i])){res = res.concat(flatten(arr[i]));
}else{res.push(arr[i]);
}
}
return res;
}
解法二:ES6
function flatten(array) {
// 只要数组中的元素有一个嵌套数组,就合并
while(array.some(item=>Array.isArray(item)))
array=[].concat(...array)
console.log(array) //[1,2,2,3,4,5,5,6,7,8,9,11,12,12,13,14,10]
return array
}
(完)