React.children.map()做了什么?

1,077 阅读4分钟

这个方法比较少用到,ant Design的Tabs用到了这个方法,主要是为了获取子组件进行拷贝子组件添加新属性以便生成新组件的操作。

先看一个例子
class Child extends Component {
    componentDidMount() {
        console.log(
            React.Children.map(this.props.children, item => {
                return [item, [item]];
            })
        );
    }

    render() {
        return (
            <div>
                {React.Children.map(this.props.children, item => [
                    item,
                    [item]
                ])}
            </div>
        );
    }
}

class ChildrenMap extends Component {
    constructor(props) {
        super(props);
    }

    render() {
        return (
            <Child>
                <div>1</div>
                <div>2</div>
            </Child>
        );
    }
}

export default ChildrenMap;

会生成

image-20200229172508593

打印出来就是如下数据结构

image-20200229172600467

可以看出来原来的props.children里面的数据 [ item, [item] ]经过React.Children.map之后平铺成了[item,item]。

下面我们结合源码来看下是怎么个实现过程:

一、mapChildren

//传入children原始数据,func方法,context很少用到忽略。
function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

主要作用是解析数据返回result一个新数组

二、mapIntoWithKeyPrefixInternal

//children原始数组,array要返回的数组,prefix:要生成的数组的key,func:原始方法
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);
}

escapeUserProvidedKey的主要作用在第二层递归后生成key,可以先不看。

const POOL_SIZE = 10;
const traverseContextPool = [];

//从对象池获取数据 对象池给个最大存放数,可以减少对象的生成次数,做到性能的优化。需要用到这个对象的时候可以直接去对象池去取。
//主要是因为在递归的时候会产生多次traverseContext对象。
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

traverseAllChildren就是单纯的调用traverseAllChildrenImpl方法。

//children是原始数据,callback即traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);中的mapSingleChildIntoContext方法我一会在看
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) {
    //mapSingleChildIntoContext回调此方法
    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];
      //不手动设置key的话第一层第一个是.0,第二个是.1
      nextName = nextNamePrefix + getComponentKey(child, i);
      //递归铺平数组
      subtreeCount += traverseAllChildrenImpl(
        child,
        nextName,
        callback,
        traverseContext,
      );
    }
  } else {
    //可迭代的对象
    const iteratorFn = getIteratorFn(children);
    if (typeof iteratorFn === 'function') {
      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,
        );
      }
    } else if (type === 'object') {
      let addendum = '';
      const childrenString = '' + children;
    }
  }
  return subtreeCount;
}

当前方法主要是遍历this.props.children判断当前item是数组、类数组还是别的类型,如果是数组、类数组递归当前traverseAllChildrenImpl方法。如果是非数组执行mapSingleChildIntoContext方法。

四、mapSingleChildIntoContext

//traverseContext即对象池中存的对象数据
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;
	//执行自身func方法,步进自加
  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    //如果根据React.Children.map()第二个参数callback,执行仍是一个数组的话,
    //递归调用mapIntoWithKeyPrefixInternal,继续之前的步骤,
    //直到是单个ReactElement
    // c => c返回自身当前节点
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    if (isValidElement(mappedChild)) {
      //赋给新对象除key外同样的属性,替换key属性
      mappedChild = cloneAndReplaceKey(
        mappedChild,
        //如果新老keys是不一样的话,两者都保留,像traverseAllChildren对待objects做的那样
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      );
    }
    //result即map时,return的result
    result.push(mappedChild);
  }
}

这是最核心的地方。

  1. 在上一步获取到当前item是非数组的情况下,执行func方法,获得当前需要显示或处理的item组件。同时利用cloneAndReplaceKey去拷贝当前item的DOM并生成的当前项的新的key值。
  2. 如果是数组递归调用mapIntoWithKeyPrefixInternal方法。注意这里的fuc传入的是c => c,不在传入原始func是为了防止死循环。

到此就完成的这个方法的全部讲解,主要是利用了两次递归,traverseAllChildrenImpl和mapSingleChildIntoContext。traverseAllChildrenImpl是获取初始数组以及mapSingleChildIntoContext生成的数组进行递归,mapSingleChildIntoContext则是通过返回的item组件执行func方法生成新的组件或者数组进行调用mapIntoWithKeyPrefixInternal方法去递归。

最后来个流程图

image-20200229190254578