React 源码浅析之 - ReactChildren

Create at 2017 09 2111 min read技术ReactSourceCode


引入的模块

var ReactElement = require("ReactElement")

var emptyFunction = require("fbjs/lib/emptyFunction")
var invariant = require("fbjs/lib/invariant")

我们来看一下 ReactElement 模块,其他两个都是工具函数,不用关心。

Export 的对象

var ReactChildren = {
  forEach: forEachChildren,
  map: mapChildren,
  count: countChildren,
  toArray: toArray,
}

module.exports = ReactChildren

依次来看一下这个四个 API

forEach

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

入参: children, forEachFunc, forEachContext. 首先通过 getPooledTraverseContext 拿到一个遍历的上下文对象 traverseContext,然后调用 traverseAllChildren 方法来遍历所有传入 children 的后代节点。 最后释放当前的 traverseContext.

getPooledTraverseContext

var POOL_SIZE = 10
var traverseContextPool = []
function getPooledTraverseContext(
  mapResult,
  keyPrefix,
  mapFunction,
  mapContext
) {
  if (traverseContextPool.length) {
    var 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 以避免每次重新创建新对象的成本,大小为 10. getPooledTraverseContext 方法接收四个参数,mapResult, keyPrefix, mapFunction, mapContext. 然后赋值到 traverseContext 上,除此之外还添加了一个计数器属性 count.

traverseAllChildren

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

  return traverseAllChildrenImpl(children, '', callback, traverseContext);
}
 
`traverseAllChildren` 只是个空壳,里面的 `traverseAllChildrenImpl` 才是真正的实现。
** traverseAllChildrenImpl **

function traverseAllChildrenImpl(
  children,
  nameSoFar,
  callback,
  traverseContext,
) {
  var type = typeof children;

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

  if (
    children === null ||
    type === 'string' ||
    type === 'number' ||
    // The following is inlined from ReactElement. This means we can optimize
    // some checks. React Fiber also inlines this logic for similar purposes.
    (type === 'object' && children.$$typeof === REACT_ELEMENT_TYPE)
  ) {
    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;
  }

接收四个参数,首先判断 children 参数的 type ,不合法都认为 children 是 null. 紧接着的一堆判断,就是说当 children 是单个合法 React 元素的时候,执行 callback 函数,并返回 1,因为后面会递归的调用当前这个函数,所以这里也是递归调用的出口。

var child;
var nextName;
var subtreeCount = 0; // Count of children found in the current subtree.
var nextNamePrefix = nameSoFar === '' ? SEPARATOR : nameSoFar + SUBSEPARATOR;

if (Array.isArray(children)) {
  for (var i = 0; i < children.length; i++) {
    child = children[i];
    nextName = nextNamePrefix + getComponentKey(child, i);
    subtreeCount += traverseAllChildrenImpl(
      child,
      nextName,
      callback,
      traverseContext,
    );
  }
} else {

当传入的 children 是一个 Array 的时候,遍历这个 Children Array , 并对里面的每个元素调用当前的函数 traverseAllChildrenImpl. 传入的参数中需要注意, nextName 用来给要遍历的 React 元素添加 key 值,callback 和 traverseContext 和当前函数的值都是一样的,保证了每个子元素也能应用当前的 callback 且能访问到原始的 traverseContext.

} else {
  var iteratorFn =
    (ITERATOR_SYMBOL && children[ITERATOR_SYMBOL]) ||
    children[FAUX_ITERATOR_SYMBOL];
  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.%s',
          getStackAddendum(),
        );
        didWarnAboutMaps = true;
      }
    }

    var iterator = iteratorFn.call(children);
    var step;
    var 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') {
    var addendum = '';
    if (__DEV__) {
      addendum =
        ' If you meant to render a collection of children, use an array ' +
        'instead.' +
        getStackAddendum();
    }
    var 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,
    );
  }
}

当不是 Array 但是一个可迭代的对象的时候,和上面一样,递归调用 traverseAllChildrenImpl 方法。 对于其他情况,认为 child 不合法,进行报错。

  return subtreeCount;
}

最后返回所有后代元素的数量。 整体上来看 traverseAllChildrenImpl 方法的作用就是,遍历给定 children 的所有后代元素,在每个后代元素上调用 callback 方法,并给每个元素分配一个当前上下文下唯一的 key 值作为参数要传入的参数。

回到 forEach 方法:

traverseAllChildren(children, forEachSingleChild, traverseContext)

这一句就是说遍历给定 children 的所有后代元素,并给它们调用 forEachSingleChild 方法。

function forEachSingleChild(bookKeeping, child, name) {
  var { func, context } = bookKeeping
  func.call(context, child, bookKeeping.count++)
}

这个传入的 callback 方法 forEachSingleChild 就是从入参 bookKeeping 也就是 traverseContext 中拿到 func 和 context,把 context 作为 func 的上下文, child 和计数器 count 作为参数进行调用。这里的 func 就是 forEachChildren 的入参 forEachFunc,也就是需用最终用户提供的函数。

releaseTraverseContext(traverseContext)

释放当前的 traverseContext 也就是把 traverseContext 的属性都置为 null 并放入 traverseContextPool 中供后续使用,提高使用效率。

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

map

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

给传入 children 的后代元素调用 func 并返回调用 func 的结果集合。

mapIntoWithKeyPrefixInternal

function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
  var escapedPrefix = ""
  if (prefix != null) {
    escapedPrefix = escapeUserProvidedKey(prefix) + "/"
  }
  var traverseContext = getPooledTraverseContext(
    array,
    escapedPrefix,
    func,
    context
  )
  traverseAllChildren(children, mapSingleChildIntoContext, traverseContext)
  releaseTraverseContext(traverseContext)
}

首先,如果传入了 prefix ,那么转义 prefix 作为 traverseContext 的 prefix key. 然后拿到一个 traverseContext 对象,接着和 forEachChildren 一样,遍历所有 children 的后代元素并执行给定的 callback 函数,最后对 traverseContext 进行释放。

唯一的不同就是这个 callback 方法:mapSingleChildIntoContext .

mapSingleChildIntoContext

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

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

这个方法和上面的 forEachSingleChildren 很像。从 bookKeeping 上拿到 result , keyPrefix, func, context. Result 其实就是 mapChildren 里面一开始定义的空数组, keyPrefix 就是 mapIntoWithKeyPrefixInternal 里面 escapedPrefix , func 和 context 都是 mapChildren 对应的入参。

首先定义 mappedChild 为用户传入的 mapFunc 函数调用的返回值,然后判断这个返回值 mappedChild 是不是一个 Array. 如果是,那么循环调用 mapIntoWithKeyPrefixInternal 方法;否则在不为 null 的情况且是一个合法 React 元素的时候,用一个通过 keyPrefix , 用户分配的 key 即 mappedChild.key 和原有的 childkey 组成新 key 值的 mappedChild 的克隆元素作为 map 的结果,push 到 result 中。

整个 mapChildren 方法,就是对提供的 children 的每个后代元素调用 mapFunc 方法,给返回的结果设置新的 key ,最后把每一个执行的结果 mappedChild 放入到一个列表中返回给用户。

countChildren

function countChildren(children, context) {
  return traverseAllChildren(children, emptyFunction.thatReturnsNull, null)
}

这个就很简单了,只是通过遍历返回所有后代节点的个数。 emptyFunction.thatReturnsNull 就是一个返回为 null 的函数。

toArray

function toArray(children) {
  var result = []
  mapIntoWithKeyPrefixInternal(
    children,
    result,
    null,
    emptyFunction.thatReturnsArgument
  )
  return result
}

理解了上面的 mapIntoWithKeyPrefixInternal ,那么这里也很简单了。 emptyFunction.thatReturnsArgument, 是一个函数,会返回它的第一个参数。

** mapSingleChildIntoContext **

var mappedChild = func.call(context, child, bookKeeping.count++)

那么这句也就是返回 child 本身了。并将结果放入到 result 里面, 最后把所有的 result 返回给调用方。

总结

ReactChildren 有四个 API ,而这四个 API 主要依赖与两个方法,traverseAllChildrenImplmapSingleChildIntoContext 其他方法都是在此之上的组合调用。 还有一个值得注意的地方,就是用到对象池 traverseContextPool 。个人认为是因为在这里经常会递归调用而频繁的需要新建 traverseContext 对象,而每次都重新新建对象需要在堆里面重新分配内存,成本比较高,所以引入了对象池,以提高性能。

相关文章

  • {% post_link react-source-code-analyze-1 React 源码浅析之 - 入口文件 %}
  • {% post_link react-source-code-analyze-2 React 源码浅析之 - ReactBaseClasses %}
  • {% post_link react-source-code-analyze-3 React 源码浅析之 - ReactChildren %}
  • {% post_link react-source-code-analyze-4 React 源码浅析之 - ReactElement %}
  • {% post_link react-source-code-analyze-5 React 源码浅析之 - onlyChildren %}

本文章遵循: CC BY-NC-ND 4.0Creative CommonsAttributionNonCommercialNoDerivatives

非商业转载请注明作者及出处,商业转载请联系 作者本人

本文标题为:React 源码浅析之 - ReactChildren

本文链接为:https://blog.kisnows.com/2017/09/21/react-source-code-analyze-3