版本基于 React 17.0.1
在网上查看 React.Children
的源码,发现最新版本与之前版本有很大的不一样
下面以 React.Children.map
为例
使用
const MapTest = (props) => {
return (
<div>
{React.Children.map(props.children, c => (<div className='bgBlue'>{c}</div>))}
</div>
)
}
const App = () => {
return (
<Fragment>
<MapTest>
<div>
<span>1</span>
<span>2</span>
</div>
<div>
<span>1</span>
<span>2</span>
</div>
</MapTest>
</Fragment>
)
}
渲染出来的结果为
借用官网的话,React.Children.map
的作用就是在每个子节点上调用一个函数,如果 children
是一个数组,它将遍历并为数组中的每个子节点调用该函数。如果子节点为 null
或是 undefined
,则此方法将返回 null
或是 undefined
,而不会返回数组
源码解析
下面从源码出发,实际在源码中的函数是 mapChildren
// react.development.js
var Children = {
map: mapChildren,
forEach: forEachChildren,
count: countChildren,
toArray: toArray,
only: onlyChild
};
exports.Children = Children;
mapChildren
children
- 如果为
null
,就直接返回null
- 不是的话,就调用
mapIntoArray
,其中返回的result
就是mapInteArray
函数中的array
- 如果为
// react.development.js
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
var result = [];
var count = 0;
mapIntoArray(children, result, '', '', function (child) {
return func.call(context, child, count++);
});
return result;
}
mapIntoArray
将 mapIntoArray
分成 3 个片段来描述
// react.development.js
function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
// 片段一
// 片段二
// 片段三
}
片段一
通过 children
的类型来判断其是否为要渲染节点
其中 React
元素也是上一章提及的 虚拟DOM,实际就是个对象,所以通过 children.$$typeof
判断其是否为 React
元素,不然像 arr.map((i) => <li key={i}>i</li>)
这样子的 children
也是数组,mapIntoArray
函数要对其进行其他处理
// 片段一
var type = typeof children;
if (type === 'undefined' || type === 'boolean') {
// All of the above are perceived as null.
children = null;
}
var 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;
}
}
}
片段二
如果是要渲染的子节点,就会调用 callback
来处理 children
(callback
就是我们 React.Children.map
写入第二个函数入参)
使用 callback
处理完后的内容如果是数组的话,则还会再递归调用 mapIntoArray
,其中 callback
改为 function(c) { return c; }
,由于传入的是数组,所以此次递归调用 mapIntoArray
就会跳过片段二的代码,直接进入片段三代码
// 片段二,简化下源码
if (invokeCallback) {
var _child = children;
var mappedChild = callback(_child);
if (Array.isArray(mappedChild)) {
var escapedChildKey = /** 处理 key 值 */;
mapIntoArray(mappedChild, array, escapedChildKey, '', function (c) {
return c;
});
} else if (mappedChild != null) {
/** 处理 key 值*/
array.push(mappedChild);
}
return 1;
}
另外可以关注下 array.push(mappedChild)
这个函数,整个 mapIntoArray
只有这个地方对 array
(也就是 map
函数最后返回的结果 result
)进行了处理,可见但凡会被处理到的节点,最后都是以一维数组的形式返回,不管 callback
函数调用返回的是几维数组(因为 React.render
渲染的缘故,我猜)
例如:
<MapTest>
<div>1</div>
</MapTest>
{React.Children.map(props.children, c => [[c, c]])}
最后返回的结果是
片段三
遍历其子元素,依次调用 mapIntoArray
得到最终结果
// 片段三
var child;
var nextName;
var subtreeCount = 0;
var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
if (Array.isArray(children)) {
for (var i = 0; i < children.length; i++) {
child = children[i];
nextName = nextNamePrefix + getElementKey(child, i);
subtreeCount += mapIntoArray(child, array, escapedPrefix, nextName, callback);
}
} else {
var iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === 'function') {
var iterableChildren = children;
var iterator = iteratorFn.call(iterableChildren);
var step;
var ii = 0;
while (!(step = iterator.next()).done) {
child = step.value;
nextName = nextNamePrefix + getElementKey(child, ii++);
subtreeCount += mapIntoArray(child, array, escapedPrefix, nextName, callback);
}
} else if (type === 'object') {
var childrenString = '' + children;
}
return subtreeCount;