在 react 源码中,给我们暴露了用于处理 props.children
相关的 API, 源码如下
const React = {
Children: {
map,
forEach,
count,
toArray,
only,
},
....
}
其中 Map 较为 核心,理解了它,其他几个 **API** 都是利用它已经定义了的一些方法。
map
像使用 Array.map
一样来使用它,和数组的区别之一是 props.children
是树形结构的,会按照深度遍历这棵树的时候的顺序,去调用提供的 mapFunction
, 这里的来看一下 map
的定义。
/**
* 使用该方法时提供的 mapFunction(child, key, index) 会被每一个孩子调用
*
* @param {children} props.children
* @param {func} mapFunction 类似于 Array.prototype.map(callback) 的 callback
* @param {context} mapFunction 执行时的上下文
* @return {object} 遍历时的结果数组
*/
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
可以看到这个里面处理了 children
为空的时候的情况,直接返回该 children
。
然后定义了一个 result
数组,该结果数组会被一直传递下去,方便往里面 push 结果。
然后调用了一个 mapIntoWithKeyPrefixInternal
方法, 下面来看看这个方法的实现。
/**
* @param {*} props.children
* @param {Array} array 结果数组
* @param {*} prefix null
* @param {function} func 每个孩子调用的 callback
* @param {obj} context func调用的上下文
*/
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
// 和 prefix 前缀相关的可以暂时不用管
/*let escapedPrefix = '';
if (prefix != null) {
escapedPrefix = escapeUserProvidedKey(prefix) + '/';
}*/
// 从 存储 context 池子中拿取一个空对象来用
const traverseContext = getPooledTraverseContext(
array,
escapedPrefix,
func,
context,
);
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
releaseTraverseContext(traverseContext);
}
首先来看两个方法 traverseContext
和 releaseTraverseContext
。
// context 池子的大小
const POOL_SIZE = 10;
// context 池
const traverseContextPool = [];
/**
* 从 context 池子中拿到一个空的 context 对象来用,然后将传进去的参数添加到 context 中,
* 并添加 count,初始值为 0,用来记录这个遍历过程中每一个被遍历到的 child 的顺序, 并被当做 mapfunction 的第三个 index 参数。然后返回 context
* @param {array} mapResult 结果数组
* @param {string} keyPrefix 前缀
* @param {funcgtion} mapFunction 每一个孩子调用的 callback
* @param {obj} mapContext mapFunction 调用的时候的上下文
*/
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,
};
}
}
// 释放使用过的 context,将参数置为初始值,如果线程池没有满,那么就讲这个
// 使用过的 context 添加进去。这样做的目的是为了防止频繁的分配内存,影响性能。
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);
}
}
可以看到这里定义了一个 context
池,大小为 10。在 getPooledTraverseContext
的时候,如果这个池子里面有 创建过的对象,那么就直接拿来用,不需要定义一个新的对象。在每一步前面提到的 mapIntoWithKeyPrefixInternal
中结束的时候,会调用 releaseTraverseContext
来释放这个对象,如果 context
池子里面未满的话,就可以将它放进去,方便后面使用。因为 props.children
很可能是一个树形结构,在后面的代码中可能还会继续调用 mapIntoWithKeyPrefixInternal
,以形成递归调用,在递归的去遍历的过程中为了避免重复的申请和销毁空间,所以定义了这个 context
池。
现在回到 mapIntoWithKeyPrefixInternal
方法中,继续看 traverseAllChildren
,它的第二个参数 mapSingleChildIntoContext
我们后面具体用到的时候再讲。
/**
* 遍历 children 实现
* @param {?*} props.children
* @param {!string} nameSoFar Name of the key path so far.
* @param {!function} callback 对每个找到的 children 调用的方法,在它的内部会调用我们使用的时候传入的那个 mapFunction,然后把结果 push 到 result 数组中。
* @param {?*} traverseContext 用于在遍历过程中传递信息。
* @return {!number} 返回当前参数 children 下有多少个孩子
*/
function traverseAllChildrenImpl(
children,
nameSoFar,
callback,
traverseContext,
) {
// -------------------------- 首先处理 单个 children 的情况
const type = typeof children;
if (type === 'undefined' || type === 'boolean') {
children = null;
}
let invokeCallback = false;
// 为 null 也会调用
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 是单个节点
if (invokeCallback) {
callback(
traverseContext,
children,
nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
);
// 只有一个节点 那么 children 数量就是 1, 该函数返回 children 的数量,所以这里直接返回 1
return 1;
}
// -------------------------- 处理children 是 Array 的情况
let child;
let nextName;
let subtreeCount = 0; // 找到的 children 的数量
// const nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
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 {
// 如果不是数组,但是有迭代器, 表示可遍历,
const iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === 'function') {
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;
}
可以看到上面的代码,在不断的递归,如果是单个节点,那么直接 调用 callback
也就是 mapSingleChildIntoContext
, 这个是这整个递归的出口,如果是数组或者其他可以迭代的,那么就递归的调用 traverseAllChildrenImpl
。然后来看一下 mapSingleChildIntoContext
。
/**
* @param {obj} bookKeeping traverseContext 前面从 context 池子拿出来转换过的 context 携带着一些信息
* @param {*} child props.children
* @param {*} childKey
*/
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
const {result, keyPrefix, func, context} = bookKeeping;
// 调用我们最开始自定义的 mapFunction,并拿到返回结果, 这里用到了 count
let mappedChild = func.call(context, child, bookKeeping.count++);
// 有可能我们自己返回的时候,返回的是数组,那么就继续回到 mapIntoWithKeyPrefixInternal 中
if (Array.isArray(mappedChild)) {
mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
} else if (mappedChild != null) {
// 如果是可用的 element, 那么 clone 一下,就像 Array.prototype.map 返回的是一个新的数组一样
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,
);
}
// 将结果 push 到结果数组中去
result.push(mappedChild);
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。