React 源码学习01 ---- React.Children.map 的实现与应用

时间:2024-11-07 11:17:47

1. 使用方法

React.Children.map(children, function[(thisArg)])

2. 方法解释

在 children 里的每个直接子节点上调用一个函数,并将 this 设置为 thisArg。如果 children 是一个数组,它将被遍历并为数组中的每个子节点调用该函数。如果子节点为 null 或是 undefined,则此方法将返回 null 或是 undefined,而不会返回数组。

3. 注意

如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历。

4. React.Children.map 源码

4.1 mapChildren 源码
\packages\react\src\ReactChildren.js

function mapChildren(
  children: ?ReactNodeList,
  func: MapFunc,
  context: mixed,
): ?Array<React$Node> {
  // 判断传入的子元素列表是否是 null 或 undefined,条件成立,直接返回 children
  if (children == null) {
    // $FlowFixMe limitation refining abstract types in Flow
    return children;
  }
  // 存储处理后的 child 列表
  const result: Array<React$Node> = [];
  // 每一个 child 的索引
  let count = 0;
  // 将 children 中的每个元素映射到数组 result 中,并在映射过程中应用 func 函数
  mapIntoArray(children, result, '', '', function (child) {
    // 调用映射函数处理 child,并返回处理结果
    return func.call(context, child, count++);
  });
  return result;
}
4.2 mapChildren 函数解释
  1. children 是任意类型的子元素列表。
  2. func 操作子元素的映射函数,接受两个参数:子元素和索引,并返回一个映射后的结果。
  3. context 执行 func 映射函数的上下文。
  4. 检查 children 是否为 null 或 undefined,如果是,则直接返回 children。
  5. 创建一个名为 result 的空数组,用于存储处理后的子元素。
  6. 初始化一个计数器 count,用于记录当前处理的子元素的索引。
  7. 使用 mapIntoArray 函数遍历 children,并将每个子元素转换为数组形式存储在 result 中。mapIntoArray 函数还接受一个回调函数,该回调函数会调用 func 函数,并传入当前子元素和计数器的值。
  8. 返回处理后的 result 数组。
4.3 mapIntoArray 函数的实现
function mapIntoArray(
  children: ?ReactNodeList,
  array: Array<React$Node>,
  escapedPrefix: string,
  nameSoFar: string,
  callback: (?React$Node) => ?ReactNodeList,
): number {
  // 使用 typeof 获取 children 的类型
  const type = typeof children;
  // 判断 type 是否是 undefined 或者 boolean,跳转成立,将 children 赋值 null
  if (type === 'undefined' || type === 'boolean') {
    // All of the above are perceived as null.
    children = null;
  }
  // 是否调用回调函数标记
  let invokeCallback = false;
  // 如果 children 是 null,则调用回调
  if (children === null) {
    invokeCallback = true;
  } else {
    // 判断 type 是 bigint、string、number 直接调用回调
    switch (type) {
      case 'bigint':
      case 'string':
      case 'number':
        invokeCallback = true;
        break;
      case 'object':
        // 如果 type 是 object,继续判断 $$typeof 的值
        switch ((children: any).$$typeof) {
          // 如果 $$typeof 是 REACT_ELEMENT_TYPE、REACT_PORTAL_TYPE直接进入回调
          case REACT_ELEMENT_TYPE:
          case REACT_PORTAL_TYPE:
            invokeCallback = true;
            break;
          // 如果 $$typeof 是 REACT_LAZY_TYPE 进行递归调用
          case REACT_LAZY_TYPE:
            const payload = (children: any)._payload;
            const init = (children: any)._init;
            return mapIntoArray(
              init(payload),
              array,
              escapedPrefix,
              nameSoFar,
              callback,
            );
        }
    }
  }
  // 如果 invokeCallback 是 true,直接调用回调
  if (invokeCallback) {
    const child = children;
    // 使用回调函数处理 child,然后赋值给 mappedChild
    let mappedChild = callback(child);
    // 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:
    // 获取子元素的key,如果 nameSoFar 是空,直接使用 SEPARATOR + getElementKey(child, 0) 否则使用 nameSoFar
    const childKey =
      nameSoFar === '' ? SEPARATOR + getElementKey(child, 0) : nameSoFar;
    // 判断处理后的子元素是否是数组【Array.isArray】 
    if (isArray(mappedChild)) {
      let escapedChildKey = '';
      // 如果 childKey 不为 null,则将 childKey 的单/替换为//,结果赋值给 escapedChildKey
      if (childKey != null) {
        escapedChildKey = escapeUserProvidedKey(childKey) + '/';
      }
      // 如果是数组,递归调用 mapIntoArray
      mapIntoArray(mappedChild, array, escapedChildKey, '', c => c);
    } else if (mappedChild != null) {
      // 如果是 react 元素
      if (isValidElement(mappedChild)) {
        // 复制一份新的子元素
        const newChild = cloneAndReplaceKey(
          mappedChild,
          // Keep both the (mapped) and old keys if they differ, just as
          // traverseAllChildren used to do for objects as children
          escapedPrefix +
            // $FlowFixMe[incompatible-type] Flow incorrectly thinks React.Portal doesn't have a key
            (mappedChild.key != null &&
            (!child || child.key !== mappedChild.key)
              ? escapeUserProvidedKey(
                  // $FlowFixMe[unsafe-addition]
                  '' + mappedChild.key, // eslint-disable-line react-internal/safe-string-coercion
                ) + '/'
              : '') +
            childKey,
        );
        mappedChild = newChild;
      }
      // 将子元素放入存储的数组中
      array.push(mappedChild);
    }
    return 1;
  }

  let child;
  let nextName;
  let subtreeCount = 0; // Count of children found in the current subtree.
  const nextNamePrefix =
    nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
  // 如果 invokeCallback 为 false,
  // children 是否为数组
  if (isArray(children)) {
    // 遍历 children 中每个元素,调用 mapIntoArray 函数,并将返回的子树计数累加到 subtreeCount 中
    for (let i = 0; i < children.length; i++) {
      child = children[i];
      nextName = nextNamePrefix + getElementKey(child, i);
      subtreeCount += mapIntoArray(
        child,
        array,
        escapedPrefix,
        nextName,
        callback,
      );
    }
  } else {
    // children 是一个可迭代对象,则使用 iteratorFn 函数获取迭代器,并遍历每个元素,调用 mapIntoArray 函数,并将返回的子树计数累加到 subtreeCount 中
    const iteratorFn = getIteratorFn(children);
    if (typeof iteratorFn === 'function') {
      const iterableChildren: Iterable<React$Node> & {
        entries: any,
      } = (children: any);
      const iterator = iteratorFn.call(iterableChildren);
      let step;
      let ii = 0;
      // $FlowFixMe[incompatible-use] `iteratorFn` might return null according to typing.
      while (!(step = iterator.next()).done) {
        child = step.value;
        nextName = nextNamePrefix + getElementKey(child, ii++);
        subtreeCount += mapIntoArray(
          child,
          array,
          escapedPrefix,
          nextName,
          callback,
        );
      }
    } else if (type === 'object') {
      // children 是一个对象,则检查其是否有 then 方法,如果有,则将其视为一个 Promise,并递归调用 mapIntoArray 函数
      if (typeof (children: any).then === 'function') {
        return mapIntoArray(
          resolveThenable((children: any)),
          array,
          escapedPrefix,
          nameSoFar,
          callback,
        );
      }
      // children 是一个对象,但没有 then 方法,则将其转换为字符串,并抛出一个错误,因为对象不能作为 React 子元素
      // eslint-disable-next-line react-internal/safe-string-coercion
      const childrenString = String((children: any));

      throw new Error(
        `Objects are not valid as a React child (found: ${
          childrenString === '[object Object]'
            ? 'object with keys {' +
              Object.keys((children: any)).join(', ') +
              '}'
            : childrenString
        }). ` +
          'If you meant to render a collection of children, use an array ' +
          'instead.',
      );
    }
  }
  // 返回 subtreeCount,表示处理的子元素总数
  return subtreeCount;
}
4.4 总结

目前是第一次看这个函数的实现,知道他大概干了什么,也大概明白每一步要干什么,但是目前还不清除他为什么要这么干。为什么要来看他的实现呢?一个是只有知道他是怎么实现的,才能知道在调用这个方法时,那些情况会报错,为什么报错,能够快速处理。使用这个方法开发功能的时候能够更加得心应手。

5. React.Children.map 应用

  1. swiper 组件的开发,或者说类 swiper 组件的开发,都需要在 swiper 组件内部获取他的子元素,然后再再子元素的外层添加一个盒子,然后在盒子上实现我们需要的动画效果,比如渐入渐出、放大缩小,滑动等动画效果;
  2. 目前想到可以使用的就是瀑布流展示效果,动态获取每一列的高度,然后在动态的分配下一个子元素在那一列展示;
  3. 其他的应用应该还有很多,只是我在开发中遇到的比较少,以后有新的想法,再来补充!