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. 其他的应用应该还有很多,只是我在开发中遇到的比较少,以后有新的想法,再来补充!
相关推荐
BillKu24 分钟前
Vue3数组去重方法总结
前端·javascript·vue.js
Asu520224 分钟前
思途SQL学习 0729
数据库·sql·学习
GDAL27 分钟前
Object.freeze() 深度解析:不可变性的实现与实战指南
javascript·freeze
江城开朗的豌豆1 小时前
Vue+JSX真香现场:告别模板语法,解锁新姿势!
前端·javascript·vue.js
摸鱼仙人~1 小时前
Vue.js 指令系统完全指南:深入理解 v- 指令
前端·javascript·vue.js
啃火龙果的兔子1 小时前
React 图标库发布到 npm 仓库
前端·react.js·npm
江城开朗的豌豆1 小时前
Vue列表渲染的坑:为什么不能用index当key?血泪教训总结!
前端·javascript·vue.js
JiaLin_Denny1 小时前
如何在在NPM发布一个React组件
前端·react.js·npm·npm组件·npm发布·npm发布组件·npm如何发布组件
江城开朗的豌豆1 小时前
Vue中key的妙用:为什么你的列表渲染总出bug?
前端·javascript·vue.js
Zz_waiting.1 小时前
Javaweb - 13 - AJAX
前端·javascript·ajax