React 源码解析(二) —— React.Children

582 阅读2分钟

版本基于 React 17.0.1

在网上查看 React.Children 的源码,发现最新版本与之前版本有很大的不一样

下面以 React.Children.map 为例

使用

const MapTest = (props) => {
    return (
        <div>
           {React.Children.map(props.children, c => (<div className='bgBlue'>{c}</div>))}
        </div>
    )
}

const App = () => {
    return (
        <Fragment>
            <MapTest>
                <div>
                    <span>1</span>
                    <span>2</span>
                </div>
                <div>
                    <span>1</span>
                    <span>2</span>
                </div>
            </MapTest>
        </Fragment>
    )
}

渲染出来的结果为

借用官网的话,React.Children.map 的作用就是在每个子节点上调用一个函数,如果 children 是一个数组,它将遍历并为数组中的每个子节点调用该函数。如果子节点为 null 或是 undefined,则此方法将返回 null 或是 undefined,而不会返回数组

源码解析

下面从源码出发,实际在源码中的函数是 mapChildren

// react.development.js
var Children = {
  map: mapChildren,
  forEach: forEachChildren,
  count: countChildren,
  toArray: toArray,
  only: onlyChild
};

exports.Children = Children;

mapChildren

  • children
    • 如果为 null,就直接返回 null
    • 不是的话,就调用 mapIntoArray,其中返回的 result 就是 mapInteArray 函数中的 array
// react.development.js
function mapChildren(children, func, context) {
  if (children == null) {
    return children;
  }

  var result = [];
  var count = 0;
  mapIntoArray(children, result, '', '', function (child) {
    return func.call(context, child, count++);
  });
  return result;
}

mapIntoArray

mapIntoArray 分成 3 个片段来描述

// react.development.js
function mapIntoArray(children, array, escapedPrefix, nameSoFar, callback) {
  // 片段一
  // 片段二
  // 片段三
}

片段一

通过 children 的类型来判断其是否为要渲染节点

其中 React 元素也是上一章提及的 虚拟DOM,实际就是个对象,所以通过 children.$$typeof 判断其是否为 React 元素,不然像 arr.map((i) => <li key={i}>i</li>) 这样子的 children 也是数组,mapIntoArray 函数要对其进行其他处理

// 片段一
var type = typeof children;

if (type === 'undefined' || type === 'boolean') {
  // All of the above are perceived as null.
  children = null;
}

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

  }
}

片段二

如果是要渲染的子节点,就会调用 callback 来处理 childrencallback 就是我们 React.Children.map 写入第二个函数入参)

使用 callback 处理完后的内容如果是数组的话,则还会再递归调用 mapIntoArray,其中 callback 改为 function(c) { return c; },由于传入的是数组,所以此次递归调用 mapIntoArray 就会跳过片段二的代码,直接进入片段三代码

// 片段二,简化下源码
if (invokeCallback) {
  var _child = children;
  var mappedChild = callback(_child);

  if (Array.isArray(mappedChild)) {
    var escapedChildKey = /** 处理 key 值 */;
    mapIntoArray(mappedChild, array, escapedChildKey, '', function (c) {
      return c;
    });
  } else if (mappedChild != null) {
    /** 处理 key 值*/
    array.push(mappedChild);
  }

  return 1;
}

另外可以关注下 array.push(mappedChild) 这个函数,整个 mapIntoArray 只有这个地方对 array(也就是 map 函数最后返回的结果 result)进行了处理,可见但凡会被处理到的节点,最后都是以一维数组的形式返回,不管 callback 函数调用返回的是几维数组(因为 React.render 渲染的缘故,我猜)

例如:

<MapTest>
    <div>1</div>
</MapTest>

{React.Children.map(props.children, c => [[c, c]])}

最后返回的结果是

片段三

遍历其子元素,依次调用 mapIntoArray 得到最终结果

// 片段三
var child;
var nextName;
var subtreeCount = 0;

var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;
if (Array.isArray(children)) {
  for (var i = 0; i < children.length; i++) {
    child = children[i];
    nextName = nextNamePrefix + getElementKey(child, i);
    subtreeCount += mapIntoArray(child, array, escapedPrefix, nextName, callback);
  }
} else {
  var iteratorFn = getIteratorFn(children);
  if (typeof iteratorFn === 'function') {
    var iterableChildren = children;
    
    var iterator = iteratorFn.call(iterableChildren);
    var step;
    var ii = 0;

    while (!(step = iterator.next()).done) {
      child = step.value;
      nextName = nextNamePrefix + getElementKey(child, ii++);
      subtreeCount += mapIntoArray(child, array, escapedPrefix, nextName, callback);
    }
  } else if (type === 'object') {
    var childrenString = '' + children;
}

return subtreeCount;

流程图

参考