React 源码解析-Part1. React.Children 之 map
在 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); } }