在 react 源码中,给我们暴露了用于处理 props.children 相关的 API, 源码如下

const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },
  ....
}
其中 Map 较为 核心,理解了它,其他几个 **API** 都是利用它已经定义了的一些方法。

map

像使用 Array.map 一样来使用它,和数组的区别之一是 props.children 是树形结构的,会按照深度遍历这棵树的时候的顺序,去调用提供的 mapFunction, 这里的来看一下 map 的定义。

/**
 * 使用该方法时提供的 mapFunction(child, key, index) 会被每一个孩子调用
 *
 * @param {children} props.children
 * @param {func} mapFunction 类似于 Array.prototype.map(callback) 的 callback
 * @param {context} mapFunction 执行时的上下文
 * @return {object} 遍历时的结果数组
 */
function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

可以看到这个里面处理了 children 为空的时候的情况,直接返回该 children

然后定义了一个 result 数组,该结果数组会被一直传递下去,方便往里面 push 结果。

然后调用了一个 mapIntoWithKeyPrefixInternal 方法, 下面来看看这个方法的实现。

/**
 * @param {*} props.children
 * @param {Array} array 结果数组
 * @param {*} prefix null
 * @param {function} func 每个孩子调用的 callback
 * @param {obj} context func调用的上下文
 */
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  // 和 prefix 前缀相关的可以暂时不用管
  /*let escapedPrefix = '';
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }*/
  // 从 存储 context 池子中拿取一个空对象来用
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}

首先来看两个方法 traverseContext releaseTraverseContext

// context 池子的大小
const POOL_SIZE = 10;
// context 池
const traverseContextPool = [];
/**
 * 从 context 池子中拿到一个空的 context 对象来用,然后将传进去的参数添加到 context 中,
 * 并添加 count,初始值为 0,用来记录这个遍历过程中每一个被遍历到的 child 的顺序, 并被当做 mapfunction 的第三个 index 参数。然后返回 context
 * @param {array} mapResult 结果数组
 * @param {string} keyPrefix 前缀
 * @param {funcgtion} mapFunction 每一个孩子调用的 callback
 * @param {obj} mapContext mapFunction 调用的时候的上下文
 */
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,
    };
  }
}

// 释放使用过的 context,将参数置为初始值,如果线程池没有满,那么就讲这个
// 使用过的 context 添加进去。这样做的目的是为了防止频繁的分配内存,影响性能。
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);
  }
}

可以看到这里定义了一个 context 池,大小为 10。在 getPooledTraverseContext 的时候,如果这个池子里面有 创建过的对象,那么就直接拿来用,不需要定义一个新的对象。在每一步前面提到的 mapIntoWithKeyPrefixInternal 中结束的时候,会调用 releaseTraverseContext来释放这个对象,如果 context 池子里面未满的话,就可以将它放进去,方便后面使用。因为 props.children 很可能是一个树形结构,在后面的代码中可能还会继续调用 mapIntoWithKeyPrefixInternal,以形成递归调用,在递归的去遍历的过程中为了避免重复的申请和销毁空间,所以定义了这个 context 池。

现在回到 mapIntoWithKeyPrefixInternal 方法中,继续看 traverseAllChildren,它的第二个参数 mapSingleChildIntoContext 我们后面具体用到的时候再讲。

/**
 * 遍历 children 实现
 * @param {?*} props.children
 * @param {!string} nameSoFar Name of the key path so far.
 * @param {!function} callback 对每个找到的 children 调用的方法,在它的内部会调用我们使用的时候传入的那个 mapFunction,然后把结果 push 到 result 数组中。
 * @param {?*} traverseContext 用于在遍历过程中传递信息。
 * @return {!number} 返回当前参数 children 下有多少个孩子
 */
function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  // -------------------------- 首先处理 单个 children 的情况
  const type = typeof children;

  if (type === 'undefined' || type === 'boolean') {
    children = null;
  }

  let invokeCallback = false;
  // 为 null 也会调用
  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;
        }
    }
  }
  // 如果 children 是单个节点
  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    // 只有一个节点 那么 children 数量就是 1, 该函数返回 children 的数量,所以这里直接返回 1
    return 1;
  }
// -------------------------- 处理children 是 Array 的情况
  let child;
  let nextName;
  let subtreeCount = 0; // 找到的 children 的数量
  // 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') {
      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,
        );
      }
    }
  }

  return subtreeCount;
}

可以看到上面的代码,在不断的递归,如果是单个节点,那么直接 调用 callback 也就是 mapSingleChildIntoContext, 这个是这整个递归的出口,如果是数组或者其他可以迭代的,那么就递归的调用 traverseAllChildrenImpl。然后来看一下 mapSingleChildIntoContext

/**
 * @param {obj} bookKeeping traverseContext 前面从 context 池子拿出来转换过的 context 携带着一些信息
 * @param {*} child props.children
 * @param {*} childKey
 */
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;
  // 调用我们最开始自定义的 mapFunction,并拿到返回结果, 这里用到了 count
  let mappedChild = func.call(context, child, bookKeeping.count++);

  // 有可能我们自己返回的时候,返回的是数组,那么就继续回到 mapIntoWithKeyPrefixInternal 中
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    // 如果是可用的 element, 那么 clone 一下,就像 Array.prototype.map 返回的是一个新的数组一样
    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,
      );
    }
    // 将结果 push 到结果数组中去
    result.push(mappedChild);
  }
}

RickyLong
501 声望27 粉丝

所有事情都有一套底层的方法论,主要找到关键点,然后刻意练习,没有刻意练习,做事情只是低效率的重复