文章目录
React.Children.map
先来看这样一段代码:
import React from 'react';
function Father(props){
console.log(props.children);
console.log(React.Children.map(props.children, item=>[item, item]));
return props.children;
}
export default () => (
<Father>
<div>hello</div>
<div>hello</div>
<div>hello</div>
</Father>
);
代码很简单,在页面上只是显示了三行"hello"而已。重点不在这里,而是在于通过console.log
在控制台输出的内容。
我们知道可以通过组件的props.children
获得该组件的子组件。那么可以想到console.log(props.children);
预期的结果应该是打印出一个长度为3的数组。
事实也确实如此,控制台中输出了一个长度为3的ReactElement
数组。
那么console.log(React.Children.map(props.children, item=>[item, item]));
会在控制台输出什么呢?
我们可能会猜想它应该与数组的map
方法一样,返回一个长度为3的二维数组,每个元素又是一个长度为2的ReactElement
数组。
可事实却并不是这样,控制台输出的是一个长度为6的ReactElement
数组。可以想见,React.map
将我们设想中的二维数组给降维了。
具体过程是怎样,需要看看源码。
源码阅读
React.js
首先在源码目录下的React.js
可以发现map
是在此处成为React.Children
的一个方法的。
接着可以发现它真正的定义是在同目录下的ReactChildren.js
文件中。
ReactChildren.js
在ReactChildren.js
文件的最尾部,可以看到map
方法在此处被导出,其真正的名字应该是mapChildren
。
mapChildren()
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
这个方法接受3个参数children
、func
和context
。
children
就是将要被遍历的子组件数组,func
是对单个子组件需要执行的函数,context
则是func
执行时this
指针所指向的对象。
mapIntoWithKeyPrefixInternal()
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);
}
这个方法本身并没有什么可说的,值得说的是其中被调用的getPooledTraverseContext
、traverseAllChildren
和releaseTraverseContext
。
getPooledTraverseContext()
getPooledTraverseContext()
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,
};
}
}
traverseContextPool
是文件中定义的对象池,POOL_SIZE
则定义了对象池的大小。
getPooledTraverseContext
方法就是从对象池中获得一个context
对象。
逻辑是若对象池已存在对象则pop
出来,并将我们调用React.map
时传入的一些参数赋值给context
对象中相应的属性。
若对象池为空,则直接返回一个新的context
对象。
对象池
对象池的作用是避免频繁的创建和销毁,以避免不必要的性能消耗和内存抖动的问题。
traverseAllChildren()
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
这个方法用以判断传入的子组件是否为空,不是空的再继续调用traverseAllChildrenImpl
方法。
值得注意的是,参数中的callback
并不是我们传入的回调函数,而是之前在mapIntoWithKeyPrefixInternal
中传入的mapSingleChildIntoContext
。
releaseTraverseContext()
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
获得到的context
对象在使用过后,会通过这个方法将对象内属性清空并重新放入对象池中(当对象池还有空间时)。
看到这里可能大家会有一个疑问。最开始的时候对象池为空,于是直接返回了一个新的context
对象,使用完之后,通过
releaseTraverseContext
方法放回对象池中。而在又一次从对象池中获取对象的过程中,获得到的正是我们刚刚放进去的那一个,那么岂不是对象池中始终只有一个对象?我们接着往下看。
traverseAllChildrenImpl()
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;
}
}
}
if (invokeCallback) {
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;
}
let child;
let nextName;
let subtreeCount = 0; // Count of children found in the current subtree.
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') {
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.',
);
didWarnAboutMaps = true;
}
}
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,
);
}
} else if (type === 'object') {
let addendum = '';
if (__DEV__) {
addendum =
' If you meant to render a collection of children, use an array ' +
'instead.' +
ReactDebugCurrentFrame.getStackAddendum();
}
const 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,
);
}
}
return subtreeCount;
}
这部分代码算得上是React.map
实现的核心之一,我们逐段逐段的看。
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;
}
}
}
这段代码是用来判断传入的子组件的,主要是判断是不是单个对象以及类型。如果满足条件,则将invokeCallback
置为true
。
if (invokeCallback) {
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;
}
若上面的代码将invokeCallback
置为true
,则调用callback
,注意此处的callback
不是我们传入的回调函数,而是mapSingleChildIntoContext
。
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,
);
}
}
这段代码则是用以处理传入的是数组的情况的。若为数组,则会遍历数组,并将数组中的每一个元素作为traverseAllChildrenImpl
的第一个参数,递归的调用自身。
mapSingleChildIntoContext
这个方法是React.map
实现的另一个核心。
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,
// 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);
}
}
在这个方法里,我们从context
对象中获取到调用React.map
时传入的回调函数,并执行。
下面这段代码解释了一开始的问题,为什么我们调用React.map
,尽管预期是一个二维数组,而返回的却是一个一维数组。
if (Array.isArray(mappedChild)) {
mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
}
如果执行我们传入的回调函数后返回的是一个数组,那么则会将这个数组作为参数,重新走一遍调用React.map
之后的流程,且此时传入的回调函数就只返回本身。
同时这段代码还解释了,对象池是不是一直只有一个对象的问题。
在当我们传入的回调函数不返回一个数组时确实是这样的,但当返回一个数组,甚至是多维数组时,在此处由于会多次重走流程,于是也会多次向对象池获取对象,然而第一次获取到的对象此时还未被放回对象池中。于是便会直接返回一个新的对象,当整个方法调用完成后,对象池中便会存在多个对象了。