React.Children 可能很多人都不知道React.Children
这个API, 一方面是因为这个API不常用, 另一方方面是跟数组处理功能差不多, 不深究实现是比较容易理解的。但是后来实际去看了一下源码之后发现,他的实现方式还是非常有趣的,尤其是map
和forEach
,我们就按照map
的流程来看一下,forEach
其实差不多,只是没有返回新的节点。
API React.Children
下的API有这些
1 2 3 4 5 6 7 const Children = { map, forEach, count, toArray, only, };
其中map
, forEach
, count
和数组的对应方法功能差不多, 只是在内部多了一些对ReactDOM
的特殊处理, toArray
方法是将类数组的children
转为一个真正的数组, only
方法是校验children
是否是一个ReactElement
, 注意, 是一个.
map流程图
源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function mapChildren (children, func, context ) { if (children == null ) { return children } const result = [] mapIntoWithKeyPrefixInternal(children, result, null , func, context) return result } function mapIntoWithKeyPrefixInternal (children, array, prefix, func, context ) { let escapedPrefix = '' if (prefix != null ) { escapedPrefix = escapeUserProvidedKey(prefix) + '/' } const traverseContext = getPooledTraverseContext( array, escapedPrefix, func, context, ) traverseAllChildren(children, mapSingleChildIntoContext, traverseContext) releaseTraverseContext(traverseContext) }
map
和forEach
的最大区别就是有没有return result
。
getPooledTraverseContext
就是从pool
里面找一个对象,releaseTraverseContext
会把当前的context
对象清空然后放回到pool
中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const POOL_SIZE = 10 const traverseContextPool = []function getPooledTraverseContext ( ) { if (traverseContextPool.length) { const traverseContext = traverseContextPool.pop() return traverseContext } else { return { } } } function releaseTraverseContext (traverseContext ) { if (traverseContextPool.length < POOL_SIZE) { traverseContextPool.push(traverseContext) } }
按照这个流程来看,是不是pool
永远都只有一个值呢,毕竟推出之后操作完了就推入了,这么循环着。答案肯定是否的,这就要讲到React.Children.map
的一个特性了,那就是对每个节点的map
返回的如果是数组,那么还会继续展开,这是一个递归的过程。接下去我们就来看看。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 function traverseAllChildren (children, callback, traverseContext ) { if (children == null ) { return 0 } return traverseAllChildrenImpl(children, '' , callback, traverseContext) } function traverseAllChildrenImpl ( children, nameSoFar, callback, traverseContext, ) { const type = typeof children if (type === 'undefined' || type === 'boolean' ) { 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 } } } if (invokeCallback) { callback( traverseContext, children, nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0 ) : nameSoFar, ) return 1 } let child let nextName let subtreeCount = 0 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' ) { } else if (type === 'object' ) { } } return subtreeCount }
这里就是一层递归了,对于可循环的children
,都会重复调用traverseAllChildrenImpl
,直到是一个节点的情况,然后调用callback
,也就是mapSingleChildIntoContext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function mapSingleChildIntoContext (bookKeeping, child, childKey ) { const { result, keyPrefix, func, context } = bookKeeping let mappedChild = func.call(context, child, bookKeeping.count++) if (Array .isArray(mappedChild)) { mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c) } else if (mappedChild != null ) { if (isValidElement(mappedChild)) { mappedChild = cloneAndReplaceKey( mappedChild, keyPrefix + (mappedChild.key && (!child || child.key !== mappedChild.key) ? escapeUserProvidedKey(mappedChild.key) + '/' : '' ) + childKey, ) } result.push(mappedChild) } }
mapSingleChildIntoContext
这个方法其实就是调用React.Children.map(children, callback)
这里的callback
,就是我们传入的第二个参数,并得到map
之后的结果。注意重点来了,如果map
之后的节点还是一个数组,那么再次进入mapIntoWithKeyPrefixInternal
,那么这个时候我们就会再次从pool
里面去context
了,而pool
的意义大概也就是在这里了,如果循环嵌套多了,可以减少很多对象创建和gc
的损耗。
而如果不是数组并且是一个合规的ReactElement
,就触达顶点了,替换一下key
就推入result
了。
React 这么实现主要是两个目的:
拆分map
出来的数组
因为对Children
的处理一般在render
里面,所以会比较频繁,所以设置一个pool
减少声明和gc
的开销