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

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. 其他的应用应该还有很多,只是我在开发中遇到的比较少,以后有新的想法,再来补充!
相关推荐
晓得迷路了12 分钟前
栗子前端技术周刊第 84 期 - Vite v7.0 beta、Vitest 3.2、Astro 5.9...
前端·javascript·vite
独立开阀者_FwtCoder15 分钟前
最全301/302重定向指南:从SEO到实战,一篇就够了
前端·javascript·vue.js
Moment25 分钟前
给大家推荐一个超好用的 Marsview 低代码平台 🤩🤩🤩
前端·javascript·github
小满zs28 分钟前
Zustand 第三章(状态简化)
前端·react.js
whoarethenext1 小时前
C++ OpenCV 学习路线图
c++·opencv·学习
明似水1 小时前
用 Melos 解决 Flutter Monorepo 的依赖冲突:一个真实案例
前端·javascript·flutter
独立开阀者_FwtCoder1 小时前
stagewise:让AI与代码编辑器无缝连接
前端·javascript·github
恰薯条的屑海鸥1 小时前
零基础在实践中学习网络安全-皮卡丘靶场(第十四期-XXE模块)
网络·学习·安全·web安全·渗透测试
江城开朗的豌豆1 小时前
JavaScript篇:对象派 vs 过程派:编程江湖的两种武功心法
前端·javascript·面试
Lester_11011 小时前
嵌入式学习笔记 - freeRTOS vTaskPlaceOnEventList()函数解析
笔记·学习