React.children.map 解析

3,982 阅读5分钟

React.children.map 使用

class App enrends Reacr.Component{
    return (
        <div>
            {React.children.map(this.props.children, item => [item,item])}
        </div>
    )
}

原理解析

React map 流程图

入口方法

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

入口方法 mapChildren 会进行 children 判断,然后决定是否进行后续操作。
核心方法 mapIntoWithKeyPrefixInternal
参数列表

  1. children: 需要处理的子组件列表
  2. result: 最终处理的结果
  3. null:这个参数不用关心,类似于一个组件 id 的前缀,防止 reactid 重复用的
  4. func:用户自定义的对每个 children 的处理函数
  5. context:处理 children 的上下文

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);  // 开始处理 children
  releaseTraverseContext(traverseContext); // 处理完毕后将 traverseContext 返还给对象池
}

这里面出现了很多新的方法,但是其核心是 traverseAllChildren, 它是真正调用 func 处理 children 并将最终结果返回。
getPooledTraverseContext 方法可以理解为 React 建立的一个对象池,这个方法就是从对象池中获取一个对象。
releaseTraverseContext 方法是将 getPooledTraverseContext 获取的对象返还给对象池。

对象池

const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext,
) {
  if (traverseContextPool.length) { // 如果对象池中有已经存在的对象,则直接此次处理的 children 数组的基本信息记录在 traverseContext 对象中
    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,
    };
  }
}

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是从对象池中获取一个对象,releaseTraverseContext 是将使用的对象再放回对象池中,但是看到 poppush 方法会感到疑惑。因为感觉上这里不管什么时候得到的都是一个对象。这里先留个悬念,后面再做解释。

traverseAllChildren 开始解析 children

function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }

  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}

其实这个方法只是做了简单的判断,然后就会调用其核心方法 traverseAllChildrenImpl。当然,他的参数列表中有一个 mapSingleChildIntoContext 方法,这个就是 callback,这个方法后续再讲解。

traverseAllChildrenImpl

function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  const type = typeof children;

  // 从这里看出,children 不能是 boolean 和 undefined
  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( // 所有子组件最终解析归宿,都会走进这个分支 mapSingleChildIntoContext === 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.children ,而那些子元素是不合法的,以及 react 在 DEV 阶段的一些处理。
这个方法最重要的是对子元素类型的处理,在这个处理过程中,通过判断子元素的类型来进行相应的处理,当子元素是 nullstringnumberREACT_ELEMENT_TYPEREACT_PORTAL_TYPE 这是就可以判断出,这个是单个的子元素,直接调用 callback === mapSingleChildIntoContext 方法进行处理。如果不是上述类型,则会进行进一步处理,但是处理的基本思路是,使用 traverseAllChildrenImpl 方法对 children 进行递归处理,直到每一个 childrenmapSingleChildIntoContext 接收处理。

mapSingleChildIntoContext

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;

  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) { // 如果执行用户自定义的回调返回值还是数组的话,就再次将数组解析,直到 children 是单个的 react 合法元素 
    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);
  }
}

可以注意到,当给出的 children 是数组时,会调用 mapIntoWithKeyPrefixInternal,这时如果对象池没有可用对象就会新创建并且在使用后将其回收。这就解释了为什么对对象池进行 poppush 操作时,获取的对象可能不是同一个的问题。

总结

总体来说,react 使用 map 处理子元素是,有以下特点:

  1. 当用户的自定义处理函数返回值是数组时,react 会接着循环这个数组,直到每个子元素都不是数组为止。也就是所谓的对 children 进行降维打击。
  2. 整体来说,map 的整个处理过程流程很清晰,采用了 对象池的思路,减少对象的创建,以避免不必要的性能消耗和内存抖动的问题。
  3. 看源码的过程需要一点耐心,不要对某个方法的实现死扣,要先了解大致流程以后,再对其实现代码进行解析,这样子记忆的会更加清晰。