React源码 React.Children

Posted wzndkj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React源码 React.Children相关的知识,希望对你有一定的参考价值。

children是什么意思呢?就是我们拿到组件内部的props的时候,有props.children这么一个属性,大部分情况下,我们直接把 props.children 渲染到 JSX 里面就可以了。很少有情况我们需要去操作这个 children 。 但是如果一旦你需要去操作这个 children 。我们推荐使用 React.children 的 api , 而不是直接去操作他。

 

虽然说我们大部分情况下拿到的 children 是合理的 react element 或者是一个数组,但是 React 有提供 api 去操作他,那么他一定是有一个合理的原因的。

 

打开 React.js 源码,找到 children
Children: {
    map,
    forEach,
    count,
    toArray,
    only,
}

这个 children 是个对象,这个对象里面有 5 个属性,这个 5 个属性看上去就跟我们数组的操作非常的像,前两个是最重要的,也就是 map 和 forEach ,就相当于一个数组的 map 和 forEach 方法,我们可以理解为意义是一样的,但是实际的操作跟数组的 map 和 forEach 会有一点的区别。 这里 map 是所有逻辑里面最复杂的一个。而 map 和 forEach 是差不多的,他们唯一的区别是一个有返回,一个没有返回,map 是通过我们传入一个方法之后,调用出来之后,返回的一个新的数组,而 forEach 是返回原数组的。

 

我们先看一个 demo,

 

import React from ‘react‘

function ChildrenDemo(props) {
  console.log(props.children)
  console.log(React.Children.map(props.children, c => [c, [c, c]]))
  return props.children
}

export default () => (
  <ChildrenDemo>
    <span>1</span>
    <span>2</span>
  </ChildrenDemo>
)

我们创建了一个组件,叫 ChildrenDemo ,然后里面包裹了两个 span 作为他的 children ,然后在他的 props.children 里面就可以拿到,第一个打印出来的就是 props.children,我们看到就是两个 element 节点。 就跟之前的 React Element 属性是一样的。第二个是通过 React.Children.map 这个 api ,然后传入 props.children 。 并且传入了一个 callback ,这个 callback 返回的是一个嵌套两层的数组,可能比较迷茫,可以先看成 c => [c, c],然后打印出来的children分别是1,1,2,2,我们是否可以理解为他最终返回的是一个展开的,因为这总共是两个节点,props.children 是两个节点,每个节点通过 map function 之后返回的是一个数组,然后 React.Children 把他给展开了,然后就变成了一个一维数组,本来是返回一个二维数组,然后这个数组里面有两个一维数组。

 

也就是这里不管嵌套几层,返回的一维数组。这就是 React.Children.map 跟 数组 .map 的一个本质的区别,数组map第一个参数是子元素,这里的第一个参数是要遍历的数组,然后返回一个一维数组。我们接下来看下他的源码是如何做的。打开 ReactChildren.js 。翻到最下面,看到

 

export {
  forEachChildren as forEach,
  mapChildren as map,
  countChildren as count,
  onlyChild as only,
  toArray,
};

mapChildren as map,这里 export 出去的 map ,在里面就是 mapChildren 这个方法

function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, func, context);
  return result;
}

找到 mapChildren 这个方法,这里面先判断这个 children 是否等于 null ,如果等于 null ,就直接返回了。然后声明了一个 result ,最终 return 的也是这个 result 。这个时候我们再去看看 forEachChildren

function forEachChildren(children, forEachFunc, forEachContext) {
  if (children == null) {
    return children;
  }
  const traverseContext = getPooledTraverseContext(
    null,
    null,
    forEachFunc,
    forEachContext,
  );
  traverseAllChildren(children, forEachSingleChild, traverseContext);
  releaseTraverseContext(traverseContext);
}

这里面的 forEachChildren 没有result ,没有返回值。这就是他们一个本质的区别。



mapChildren 里面调用了 mapIntoWithKeyPrefixInternal,传入了 children , result 是我们刚刚声明的,第三个是 null, function 是我们传入的第二个参数,context 就是 this.object ,一般我们不用他,就不管这个东西 
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  let escapedPrefix = ‘‘;
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + ‘/‘;
  }
  const traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context,
  );
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
  releaseTraverseContext(traverseContext);
}

mapIntoWithKeyPrefixInternal 里面我们看到他先处理了一下 key,这个 key 先忽略,因为他就是一个字符串处理相关的东西,没有什么特别的,下面再调用这个三个函数,跟 forEachChildren 对比下,是差不多的。通过调用 getPooledTraverseContext ,然后去获取了一个 traverseContext ,这个东西有什么意义呢?我们直接看下这个方法 

 

const traverseContextPool = [];
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,
    };
  }
}

我们看到这个方法其实没有什么特别的处理,他首先判断一下这个全局变量 traverseContextPool ,他是否是已经存在的一个节点,如果有的话,从这个 traverseContextPool 里面 pop 一个,pop后,再把传入进来的直接挂载到他上面,没有做任何其他的操作,其实就是用来记录的对象,如果没有,就 return 一个新的对象,这有什么意义呢?回过头去看一下,后续两个方法调用了 traverseContext, 最后一句话代码是 releaseTraverseContext(traverseContext) , releaseTraverseContext 他又是什么意思呢?

 

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

我们发现,他就是一个把 traverseContext 这个对象的内容都给清空了,然后判断 traverseContextPool 是否小于 POOL_SIZE 这个最大的限制大小,就是10,如果没有大于,就往里面 push 这个对象。那这有什么意义呢?这就是一个很简单的一个对象池的一个概念,就是我这个 map function 很可能是我经常要调用的方法,如果他展开层数比较多,那么我这个 traverseContextPool 声明的对象也会比较多,如果这个声明的对象比较多,我每次调用 map functioin, 都要声明这么多对象,然后调用完了之后,又要把这么多对象释放,这其实是一个非常消耗性能的一个操作,因为一个声明对象,和一个删除对象,那么他很可能会造成内存抖动的问题,然后让我们整体看到的浏览器内页面的性能会比较差,所以在这里他设置了这么一个 traverseContextPool 。然后总共的长度给他一个10,然后是个渐进的过程,一开始他是一个空数组,随着对象一个个创建,他会把他推进去,然后又把他拿出来复用,因为我们知道 js 是一个单线程的我们在执行某一个 props.children 的时候,可以复用空间。然后我们继续看 traverseAllChildren 

 

function traverseAllChildren(children, callback, traverseContext) {
  if (children == null) {
    return 0;
  }


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

这个方法其实没有什么特别的东西,然后他 return 的其实是一个 traverseAllChildrenImpl。

function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  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;
        }
    }
  }


  if (invokeCallback) {
    callback(
      traverseContext,
      children,
      // 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.
      nameSoFar === ‘‘ ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
    );
    return 1;
  }


  let child;
  let nextName;
  let subtreeCount = 0; // Count of children found in the current subtree.
  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‘) {
      if (__DEV__) {
        // Warn about using Maps as children
        if (iteratorFn === children.entries) {
          warning(
            didWarnAboutMaps,
            ‘Using Maps as children is unsupported and will likely yield ‘ +
              ‘unexpected results. Convert it to a sequence/iterable of keyed ‘ +
              ‘ReactElements instead.‘,
          );
          didWarnAboutMaps = true;
        }
      }


      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 = ‘‘;
      if (__DEV__) {
        addendum =
          ‘ If you meant to render a collection of children, use an array ‘ +
          ‘instead.‘ +
          ReactDebugCurrentFrame.getStackAddendum();
      }
      const childrenString = ‘‘ + children;
      invariant(
        false,
        ‘Objects are not valid as a React child (found: %s).%s‘,
        childrenString === ‘[object Object]‘
          ? ‘object with keys {‘ + Object.keys(children).join(‘, ‘) + ‘}‘
          : childrenString,
        addendum,
      );
    }
  }


  return subtreeCount;
}

然后这个方法就是一个重点了,如果这里的 children 是一个单个的节点,他不是一个数组,那么这个时候他会进入这里面的判断,是undefined, string , number , object 等等。object 是一个 REACT_ELEMENT_TYPE 或者 REACT_PORTAL_TYPE 。他们都是合理的 react 可以渲染的节点,然后直接调用 invokeCallback 赋值为 true, invokeCallback 为 true 是,直接调用 callback,这里的 callback 是传入的 mapSingleChildIntoContext 这样的一个方法,他们都有一个共同的特点,就是他们不是数组或者可遍历的对象,所以他们是单个节点,所以对于单个节点,就可以直接调用 callback。如果是个数组怎么办,如果是个数组,他就会去循环遍历这个数组,然后他再调用他自身,然后再把这个 child 传进去,这就是一个递归的过程,直到单个的时候,再去调用这个 callback,那么这个 callback 是什么呢, mapSingleChildIntoContext

function mapSingleChildIntoContext(bookKeeping, child, childKey) {
  const {result, keyPrefix, func, context} = bookKeeping;


  let mappedChild = func.call(context, child, bookKeeping.count++);
  if (Array.isArray(mappedChild)) {
    mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
  } else if (mappedChild != null) {
    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);
  }
}

这个方法里面做了什么呢,传入了三个参数,第一个参数就是 bookKeeping, 也就是 traverseContext,也就是 pool里面的东西。第二个参数 child 就是 traverseAllChildrenImpl 里面的直到单个的 children, childKey 就是一个映射关系,这个可以忽略。

 

然后里面调用了 bookKeeping 里面的 func ,就是 traverseContext 里面的 func,就是那个回调的函数。然后把节点传入,再传入一个 index, 调用了之后返回给 mappedChild,然后判断是否是一个数组,如果是一个数组,会再次调用 mapIntoWithKeyPrefixInternal ,这就是一个大的递归,这时候递归的时候就不调用 func ,而是自己的 c=>c,直接返回这个节点,不然就无限循环了,对于 map 过的 chilren , 直接返回当前节点就可以了。最终,会判断下 mappedChild 是否是合理的 element, 也就是 isValidElement ,判断之后会判断 cloneAndReplaceKey,

 

export function cloneAndReplaceKey(oldElement, newKey) {
  const newElement = ReactElement(
    oldElement.type,
    newKey,
    oldElement.ref,
    oldElement._self,
    oldElement._source,
    oldElement._owner,
    oldElement.props,
  );


  return newElement;
}

这里 cloneAndReplaceKey 也非常简单,他其实就是 return 了一个新的 react element。除了一个 newKey 之外,其他都是在原来的 react element 的基础上返回

 

这就是 map function 两层嵌套递归的过程,把所有数组递归,然后把他展开返回的一个过程,所以这里 getPooledTraverseContext 里面的 pool ,有多少个数组,就有多少个对象,这就是 pool 设置在这里的含义,他不仅仅就是一层的情况下,永远都只有一个对象,如果数组里面是有嵌套的,而且嵌套是比较多的情况下,那么他的意义就比较大了。这就是 React.Children.map的实现过程。 forEachChildren 跟 mapChildren 类似。下面还有几个, toArray



function toArray(children) {
  const result = [];
  mapIntoWithKeyPrefixInternal(children, result, null, child => child);
  return result;
}

toArray就比较简单了,只是 mapChildren 里面的一层。

 

function onlyChild(children) {
  invariant(
    isValidElement(children),
    ‘React.Children.only expected to receive a single React element child.‘,
  );
  return children;
}

onlyChild 其实就是判断是否是单个合理的 react element节点 。

 

以上是关于React源码 React.Children的主要内容,如果未能解决你的问题,请参考以下文章

Next.js:错误:React.Children.only 预期接收单个 React 元素子项

React.Children.only 预计在使用 Ref 时会收到单个 React 元素子错误

React.children.only 期望接收单个反应元素子导航器

react children技巧总结

React.Children.only 期望接收单个 React 元素子元素。?

React.Children