引入的模块
var ReactElement = require("ReactElement") var emptyFunction = require("fbjs/lib/emptyFunction") var invariant = require("fbjs/lib/invariant")
我们来看一下 ReactElement 模块,其他两个都是工具函数,不用关心。
Export 的对象
var ReactChildren = { forEach: forEachChildren, map: mapChildren, count: countChildren, toArray: toArray, } module.exports = ReactChildren
依次来看一下这个四个 API
forEach
function forEachChildren(children, forEachFunc, forEachContext) { if (children == null) { return children } var traverseContext = getPooledTraverseContext( null, null, forEachFunc, forEachContext ) traverseAllChildren(children, forEachSingleChild, traverseContext) releaseTraverseContext(traverseContext) }
入参: children, forEachFunc, forEachContext. 首先通过 getPooledTraverseContext
拿到一个遍历的上下文对象 traverseContext
,然后调用 traverseAllChildren 方法来遍历所有传入 children 的后代节点。 最后释放当前的 traverseContext
.
getPooledTraverseContext
var POOL_SIZE = 10 var traverseContextPool = [] function getPooledTraverseContext( mapResult, keyPrefix, mapFunction, mapContext ) { if (traverseContextPool.length) { var 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, } } }
定义了一个 traverseContextPool
以避免每次重新创建新对象的成本,大小为 10. getPooledTraverseContext
方法接收四个参数,mapResult, keyPrefix, mapFunction, mapContext. 然后赋值到 traverseContext 上,除此之外还添加了一个计数器属性 count.
traverseAllChildren
function traverseAllChildren(children, callback, traverseContext) { if (children == null) { return 0; } return traverseAllChildrenImpl(children, '', callback, traverseContext); } `traverseAllChildren` 只是个空壳,里面的 `traverseAllChildrenImpl` 才是真正的实现。 ** traverseAllChildrenImpl ** function traverseAllChildrenImpl( children, nameSoFar, callback, traverseContext, ) { var type = typeof children; if (type === 'undefined' || type === 'boolean') { // All of the above are perceived as null. children = null; } if ( children === null || type === 'string' || type === 'number' || // The following is inlined from ReactElement. This means we can optimize // some checks. React Fiber also inlines this logic for similar purposes. (type === 'object' && children.$$typeof === REACT_ELEMENT_TYPE) ) { 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. nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar, ); return 1; }
接收四个参数,首先判断 children 参数的 type ,不合法都认为 children 是 null. 紧接着的一堆判断,就是说当 children 是单个合法 React 元素的时候,执行 callback 函数,并返回 1,因为后面会递归的调用当前这个函数,所以这里也是递归调用的出口。
var child; var nextName; var subtreeCount = 0; // Count of children found in the current subtree. var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR; if (Array.isArray(children)) { for (var i = 0; i < children.length; i++) { child = children[i]; nextName = nextNamePrefix + getComponentKey(child, i); subtreeCount += traverseAllChildrenImpl( child, nextName, callback, traverseContext, ); } } else {
当传入的 children 是一个 Array
的时候,遍历这个 Children Array , 并对里面的每个元素调用当前的函数 traverseAllChildrenImpl
. 传入的参数中需要注意, nextName 用来给要遍历的 React 元素添加 key 值,callback 和 traverseContext
和当前函数的值都是一样的,保证了每个子元素也能应用当前的 callback 且能访问到原始的 traverseContext
.
} else { var iteratorFn = (ITERATOR_SYMBOL && children[ITERATOR_SYMBOL]) || children[FAUX_ITERATOR_SYMBOL]; 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.%s', getStackAddendum(), ); didWarnAboutMaps = true; } } var iterator = iteratorFn.call(children); var step; var ii = 0; while (!(step = iterator.next()).done) { child = step.value; nextName = nextNamePrefix + getComponentKey(child, ii++); subtreeCount += traverseAllChildrenImpl( child, nextName, callback, traverseContext, ); } } else if (type === 'object') { var addendum = ''; if (__DEV__) { addendum = ' If you meant to render a collection of children, use an array ' + 'instead.' + getStackAddendum(); } var 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, ); } }
当不是 Array 但是一个可迭代的对象的时候,和上面一样,递归调用 traverseAllChildrenImpl 方法。 对于其他情况,认为 child 不合法,进行报错。
return subtreeCount; }
最后返回所有后代元素的数量。 整体上来看 traverseAllChildrenImpl
方法的作用就是,遍历给定 children 的所有后代元素,在每个后代元素上调用 callback 方法,并给每个元素分配一个当前上下文下唯一的 key 值作为参数要传入的参数。
回到 forEach 方法:
traverseAllChildren(children, forEachSingleChild, traverseContext)
这一句就是说遍历给定 children 的所有后代元素,并给它们调用 forEachSingleChild 方法。
function forEachSingleChild(bookKeeping, child, name) { var { func, context } = bookKeeping func.call(context, child, bookKeeping.count++) }
这个传入的 callback 方法 forEachSingleChild
就是从入参 bookKeeping 也就是 traverseContext
中拿到 func
和 context,把 context 作为 func
的上下文, child 和计数器 count 作为参数进行调用。这里的 func
就是 forEachChildren
的入参 forEachFunc
,也就是需用最终用户提供的函数。
releaseTraverseContext(traverseContext)
释放当前的 traverseContext
也就是把 traverseContext
的属性都置为 null
并放入 traverseContextPool
中供后续使用,提高使用效率。
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) } }
map
function mapChildren(children, func, context) { if (children == null) { return children } var result = [] mapIntoWithKeyPrefixInternal(children, result, null, func, context) return result }
给传入 children 的后代元素调用 func 并返回调用 func 的结果集合。
mapIntoWithKeyPrefixInternal
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) { var escapedPrefix = "" if (prefix != null) { escapedPrefix = escapeUserProvidedKey(prefix) + "/" } var traverseContext = getPooledTraverseContext( array, escapedPrefix, func, context ) traverseAllChildren(children, mapSingleChildIntoContext, traverseContext) releaseTraverseContext(traverseContext) }
首先,如果传入了 prefix ,那么转义 prefix 作为 traverseContext
的 prefix key. 然后拿到一个 traverseContext
对象,接着和 forEachChildren
一样,遍历所有 children 的后代元素并执行给定的 callback 函数,最后对 traverseContext
进行释放。
唯一的不同就是这个 callback 方法:mapSingleChildIntoContext
.
mapSingleChildIntoContext
function mapSingleChildIntoContext(bookKeeping, child, childKey) { var { result, keyPrefix, func, context } = bookKeeping var mappedChild = func.call(context, child, bookKeeping.count++) if (Array.isArray(mappedChild)) { mapIntoWithKeyPrefixInternal( mappedChild, result, childKey, emptyFunction.thatReturnsArgument ) } else if (mappedChild != null) { if (ReactElement.isValidElement(mappedChild)) { mappedChild = ReactElement.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 ) } result.push(mappedChild) } }
这个方法和上面的 forEachSingleChildren
很像。从 bookKeeping 上拿到 result , keyPrefix, func, context. Result 其实就是 mapChildren
里面一开始定义的空数组, keyPrefix 就是 mapIntoWithKeyPrefixInternal
里面 escapedPrefix , func 和 context 都是 mapChildren
对应的入参。
首先定义 mappedChild
为用户传入的 mapFunc
函数调用的返回值,然后判断这个返回值 mappedChild
是不是一个 Array
. 如果是,那么循环调用 mapIntoWithKeyPrefixInternal
方法;否则在不为 null
的情况且是一个合法 React 元素的时候,用一个通过 keyPrefix , 用户分配的 key 即 mappedChild.key
和原有的 childkey 组成新 key 值的 mappedChild
的克隆元素作为 map 的结果,push 到 result 中。
整个 mapChildren
方法,就是对提供的 children 的每个后代元素调用 mapFunc
方法,给返回的结果设置新的 key ,最后把每一个执行的结果 mappedChild
放入到一个列表中返回给用户。
countChildren
function countChildren(children, context) { return traverseAllChildren(children, emptyFunction.thatReturnsNull, null) }
这个就很简单了,只是通过遍历返回所有后代节点的个数。 emptyFunction.thatReturnsNull
就是一个返回为 null 的函数。
toArray
function toArray(children) { var result = [] mapIntoWithKeyPrefixInternal( children, result, null, emptyFunction.thatReturnsArgument ) return result }
理解了上面的 mapIntoWithKeyPrefixInternal ,那么这里也很简单了。 emptyFunction.thatReturnsArgument, 是一个函数,会返回它的第一个参数。
** mapSingleChildIntoContext **
var mappedChild = func.call(context, child, bookKeeping.count++)
那么这句也就是返回 child 本身了。并将结果放入到 result 里面, 最后把所有的 result 返回给调用方。
总结
ReactChildren 有四个 API ,而这四个 API 主要依赖与两个方法,traverseAllChildrenImpl
和 mapSingleChildIntoContext
其他方法都是在此之上的组合调用。 还有一个值得注意的地方,就是用到对象池 traverseContextPool
。个人认为是因为在这里经常会递归调用而频繁的需要新建 traverseContext
对象,而每次都重新新建对象需要在堆里面重新分配内存,成本比较高,所以引入了对象池,以提高性能。
相关文章
- {% post_link react-source-code-analyze-1 React 源码浅析之 - 入口文件 %}
- {% post_link react-source-code-analyze-2 React 源码浅析之 - ReactBaseClasses %}
- {% post_link react-source-code-analyze-3 React 源码浅析之 - ReactChildren %}
- {% post_link react-source-code-analyze-4 React 源码浅析之 - ReactElement %}
- {% post_link react-source-code-analyze-5 React 源码浅析之 - onlyChildren %}