React | 2020-06-30 21:19:40 555次 0次
ReactChildren 是一个比较独立的模块,所以理解起来就相对来说轻松很多,参照它的测试用例来看会更加清晰。
一、forEach使用
import React from 'react' function ChildrenTest(props) { React.Children.forEach(props.children, (vnode, index) => { console.log(vnode) }) return props.children } export default () => ( <ChildrenTest> <span key = "span1"> <em key = "em">1</em> </span> <span>2</span> <span>3</span> </ChildrenTest> )
二、forEach源码
所有的逻辑都在 ReactChildren.js 中,可以根据 __tests__ 中的测试用例了解更多的用法。
//导出别名 export { forEachChildren as forEach, mapChildren as map, countChildren as count, onlyChild as only, toArray, };
从 forEachChildren 入手,其中为了更清晰的理解,可直接忽略第三个参数:
function forEachChildren(children, forEachFunc, forEachContext) { if (children == null) { return children; } const traverseContext = getPooledTraverseContext( null, null, forEachFunc, forEachContext, ); traverseAllChildren(children, forEachSingleChild, traverseContext); releaseTraverseContext(traverseContext); }
这里用到了对象池的概念,减少垃圾回收,节省 CPU 开销。在进入 getPooled... 之前先看下 releaseTraverseContext 可能会更好的理解这一点,将当前对象中数据重置为 null 然后根据设置的对象池的大小往池中存入对象,这样避免的对象的重复创建和销毁操作,一直保持着固定数量的引用:
//对象池中回收数据 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); } }
getPooledTraverseContext 就是从对象池中取数据了,首先判断如果池子中数据为空则先创建对象,反之取出一个对象并挂载上数据,最大数量为10,超过这个数量后则无法复用数据,只能重新创建一个对象:
//对象池的数据总量 减少GC const POOL_SIZE = 10; //对象池 const traverseContextPool = []; 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, }; } }
traverseAllChildren 被上述各个方法调用,里面最终执行的是 traverseAllChildrenImpl:
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') { // All of the above are perceived as null. 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; } } } //只有一个子节点直接触发回调 forEachSingleChild if (invokeCallback) { callback( traverseContext, children, nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar, ); return 1; } ... return subtreeCount; }
这个函数有点长,分布拆开来看,invokeCallback 这个判断只有一个节点时会触发,调用 forEachSingleChild,它做的事情就是触发用户传入的回调,第一个参数为子节点,第二个参数为索引。
继续上面的方法,如果有多个子节点则是一个数组类型,直接递归调用就行:
function traverseAllChildrenImpl( children, nameSoFar, callback, traverseContext, ) { ... 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]; //key拼接 nextName = nextNamePrefix + getComponentKey(child, i); //递归调用 subtreeCount += traverseAllChildrenImpl( child, nextName, callback, traverseContext, ); } } ... return subtreeCount; }
getComponentKey 中调用 escape 将组件的 key 传入,为了安全做出字符转换:
function escape(key) { const escapeRegex = /[=:]/g; //规则字符转换 const escaperLookup = { '=': '=0', ':': '=2', }; //转为字符串后 如果规则匹配则替换 const escapedString = ('' + key).replace(escapeRegex, function(match) { return escaperLookup[match]; }); return '$' + escapedString; }
到这里基本上 forEach 循环就完事了,但是 react 中还支持一个创建节点的操作:
const threeDivIterable = { '@@iterator': function() { let i = 0; return { next: function() { if (i++ < 3) { return {value: <div key={'#' + i} />, done: false}; } else { return {value: undefined, done: true}; } }, }; }, }; <div>{ threeDivIterable }</div>
所以还要继续判断是否是 iterator 方式:
... // 就是直接取出 @@iterator属性对应的值 Function const iteratorFn = getIteratorFn(children); if (typeof iteratorFn === 'function') { //常量 false if (disableMapsAsChildren) { ... } ... 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, ); } } ...
最后判断下如果是对象则直接提示错误,不支持。
三、Map 用法
import React from 'react' function ChildrenTest(props) { console.log(React.Children.map(props.children, c => c)) return props.children } //支持扁平化数组 export default () => ( <ChildrenTest> { [[1, 2, 3], [4, 5], 6] } </ChildrenTest> )
四、Map 源码
有了上面 forEach 的源码解析,再来理解 map 会相对轻松很多,还是忽略掉 context 参数。
function mapChildren(children, func, context) { if (children == null) { return children; } const result = []; mapIntoWithKeyPrefixInternal(children, result, null, func, context); return result; }
mapIntoWithKeyPrefixInternal 和 forEachChildren 做的事情类似:
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) { let escapedPrefix = ''; if (prefix != null) { // '/test' ==> '//test/' 安全转换 escapedPrefix = escapeUserProvidedKey(prefix) + '/'; } //取对象 const traverseContext = getPooledTraverseContext( array, escapedPrefix, func, context, ); traverseAllChildren(children, mapSingleChildIntoContext, traverseContext); //销毁回归进入对象池 releaseTraverseContext(traverseContext); }
区别就是传入的回调函数发生了巨大的改变 mapSingleChildIntoContext,仍然是在 invokeCallback 为 true 是被执行:
function mapSingleChildIntoContext(bookKeeping, child, childKey) { const {result, keyPrefix, func, context} = bookKeeping; //这里如果用户返回一个数组的话 c => [c, [c]] 则会递归执行 let mappedChild = func.call(context, child, bookKeeping.count++); if (Array.isArray(mappedChild)) { mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c); } else if (mappedChild != null) { //正常情况下会进入这里 判断是否为 reactElement 对象(节点) if (isValidElement(mappedChild)) { // 克隆一个对象 通过 ReactElement 创建 mappedChild = cloneAndReplaceKey( mappedChild, keyPrefix + (mappedChild.key && (!child || child.key !== mappedChild.key) ? escapeUserProvidedKey(mappedChild.key) + '/' : '') + childKey, ); } //存入结果集 result.push(mappedChild); } }
注意 map 方法中最终返回了 result,forEach 中没有返回值,这是他们两个唯一的区别。
五、toArray源码
function toArray(children) { const result = []; // 固定了 最后一个函数参数 mapIntoWithKeyPrefixInternal(children, result, null, child => child); return result; }
其余的和 map 方法一致,下一篇继续进入 reactDom.render 方法。
0人赞