文件目录

/packages/react/src/ReactChildren

文档

文档地址

ReactChildren 主要用于组合模式,详细可以去看看ant-design的Radio.Group, CheckBox.Group;

children

this.props.children 其实是一个 ReactElement对象或者是一个数组它的值也是ReactElement, 查看demo可以看到控制台的输出。

React.Children.map

1592548713_1_.jpg
React.Children.map(this.props.children, (item) => [item, [item, [item]]])可以看到一个挺有趣的现象,多层嵌套的数组平铺成一维数组,即[item, [item, [item]]] => [item, item, item], 但可以注意一下各item的key

源码

先看看 mapChildren

/**
 * @param {?*} children  
 * @param {function(*, int)} func 遍历的方式
 * @param {*} context 上下文
 * @return {object} 遍历完后的结果
 */
function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  // 遍历出来的元素会丢到 result 中最后返回出去
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

mapIntoWithKeyPrefixInternal

/**
 * @param {?*} children   <p>123<p>
 * @param {Array} array   []
 * @param {string} prefix ""
 * @param {func} func     item => [item, [item, [item]]]
 * @param {*} context     undefined
*/

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {

  // 这里是处理 key, 看下面
  let escapedPrefix = '';
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + '/';
  }
  // getPooledTraverseContext 和 releaseTraverseContext 是配套的函数
  // 用处其实很简单,就是维护一个大小为 10 的对象重用池
  // 每次从这个池子里取一个对象去赋值,用完了就将对象上的属性置空然后丢回池子

  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);

  // 将当前 traverseContext 都的属性置空,然后丢回池子
  releaseTraverseContext(traverseContext);
}

escapeUserProvidedKey

eg. ".0/.0", ".0/.1:0", ".0/.1:1:0"/ 匹配,然后在/加一个/ => ".0//.0",

replace 的 第二个参数 '$&' 表示匹配的内容

const userProvidedKeyEscapeRegex = /\/+/g;
function escapeUserProvidedKey(text) {
  return ('' + text).replace(userProvidedKeyEscapeRegex, '$&/');
}

getPooledTraverseContext 和 releaseTraverseContext

getPooledTraverseContextreleaseTraverseContext 是配套使用的,他们主要是维护一个长度为10对象池,getPooledTraverseContext 就是从对象池中拿一个对象出来然后赋值,releaseTraverseContext 就是将 traverseContext 的属性赋值为空,然后重新放会到池子里面。这样做是因为减少 创建对线和释放对象的性能消耗。

/**
 * 从池中拿一个对象然后赋值,要是池子没有那直接返回一个对象
 * @param {Array} mapResult  遍历后的结果存放
 * @param {string} keyPrefix key 值
 * @param {func} maoFunction (item) => {}
 * @param {*} mapContext undefined 
 * @returns {result, keyPrefix, func, context, count = 0}
*/

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,
    };
  }
}

// 将对象置空然后放回池子
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);
  }
}

traverseAllChildren

/**
 * @param {*} children <p>123</p> 或者是 [<p>123</p>, <p>456</p>]
 * @param callback mapSingleChildIntoContext
 * @param {result, keyPrefix, func, context, count = 0} traverseContext
*/
function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }

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

traverseAllChildrenImple

/**
 * @param {?*} children <p>123</p> 或者是 [<p>123</p>, <p>456</p>]
 * @param {!string} nameSoFar 名字路径
 * @param {!function} callback mapSingleChildIntoContext
 * @param {?*} traverseContext {result, keyPrefix, func, context, count = 0} 
 * @return {!number} The number of children in this subtree.
 */
function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  // 这个函数核心作用就是通过把传入的 children 数组通过遍历摊平成单个节点
  // 然后去执行 mapSingleChildIntoContext

  // 开始判断 children 的类型
  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;
        }
    }
  }
  // 如果 children 是可以渲染的节点的话, 比如是<Demo>123</Demo>, 这种情况下,children 是“123”, 就直接调用 callback, 
  // 如果 children 是 [<p>123</p>, <p></p>] 就跳过往下走
  // callback 是 mapSingleChildIntoContext
  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }

  // nextName 和 nextNamePrefix 都是在处理 key 的命名
  let child;
  let nextName;
  let subtreeCount = 0; // Count of children found in the current subtree.
  const nextNamePrefix =
    nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

  // 节点是数组的话,就开始遍历数组,并且把数组中的每个元素再递归执行 traverseAllChildrenImpl
  // 如果children 是数据的话,遍历children数组, 然后在对每个元素进行
  // 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,
      );
    }
  } else {
    // 不是数组的话,就看看 children 是否可以支持迭代
    // 就是通过 obj[Symbol.iterator] 的方式去取
    const iteratorFn = getIteratorFn(children);
    // 只有取出来对象是个函数类型才是正确的
    if (typeof iteratorFn === 'function') {
      // 然后就是执行迭代器,重复上面 if 中的逻辑了
      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;
}
  • 该函数用于平铺节点
  • 我们要记住callback 是 mapSingleChildIntoContext
  • 判断children类型
    • 为数字,字符串,还有单个节点的时候直接执行mapSingleChildIntoContext
    • 要是为数组的话遍历数组,再执行traverseAllChildrenImpl
    • 不是数组的话判断一下children 是不是可迭代的,要是是对象的话就抛出对象

mapSingleChildIntoContext

/**
 * 这个函数只有当传入的 child 是单个节点是才会调用
 * @param bookKeeping traverseContext 
 * @param child 传入的节点
 * @param childKey 节点的 key
 */
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;
  // func  => (item) => [item, [item, [item]]]
  let mappedChild = func.call(context, child, bookKeeping.count++);
  // 判断函数返回值是否为数组
  // mappedChild 的结果是 [item, [item, [item]]]
  // 我们说 (item) => [item, [item, [item]]] 会平铺成 [item, item, item]
  if (Array.isArray(mappedChild)) {
    // 是数组的话就回到最先调用的函数中
    // 然后回到之前 traverseAllChildrenImpl 摊平数组的问题
    // 假如 c => [item, [item, [item]]],当执行这个函数时,返回值应该是 [item, [item, [item]]]
    // 然后 [item, [item, [item]]] 会被当成 children 传入
    // traverseAllChildrenImpl 内部逻辑判断是数组又会重新递归执行

    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    // 不是数组且返回值不为空,判断返回值是否为有效的 Element
    // 是的话就把这个元素 clone 一遍并且替换掉 key
    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);
  }
}

看看流程
eg.

  • React.Children.map(this.props.children, (item) => [item, [item, [item]]])
  • children => [<p>123</p>, <p>456</p>], func => (item) => [item, [item, [item]], result = []
  1. 先进行 mapIntoWithKeyPrefixInternal, 传入上面三个值, 我们直接忽略key

  2. 然后从池里赋值 funcresult, prefix, 此时 traverseContext

    •  result: [],
       keyPrefix: '',
       func: (item) => [item, [item, item]],
       context: undefined
       count: 0
  3. 然后执行 traverseAllChildren

  4. 再执行 traverseAllChildrenImpl, 判断 当前children 是不是可以直接渲染

    • 是的话:那直接执行 mapSingleChildIntoContext, 将当前节点放到result

    • 但现在我们的 children 是数组, 所以我们要逐个遍历, 再执行traverseAllChildrenImpl

      • eg.此时我们拿到children[0], 然后执行traverseAllChildrenImpl,然后进入 mapSingleChildIntoContext,

      • // mapSingleChildIntoContext
        // bookKeeping:{ result: [], keyPrefix: "", func: (item) => [item, [item, [item]]] }
        // child: <p>123</p>
      • mapSingleChildIntoContext内执行了func, 所以得到的结果是mappedChild = [item, [item, [item]]], 然后判断mappedChild是否是数组,是的话把 mappedChild 当成children 从第一步开始, 但递归后此时func是c => c,

      • traverseAllChildrenImpl 判断 mappedChild 是数组,然后遍历他,拿出第一个item,再执行了一次 traverseAllChildrenImpl,然后是ReactElement,就执行了mapSingleChildIntoContext, 但注意此时的func 是 c => c, 判定结果不是数组,将结果push 进result,mapSingleChildIntoContext结束。回到traverseAllChildrenImpl, 然后执行 mappedChild[1], 又在traverseAllChildrenImpl方法判定是数组,再取mappedChild[1][0] 去做 mapSingleChildIntoContext, 然后执行 func c => c,再push进result,mapSingleChildIntoContext结束, 再一次回到traverseAllChildrenImpl,直到mapChildren遍历完。

  5. 以上如此类推

流程图