最开始React.Children这个 API 是不想讲的,一方面平时不怎么用,另一方面跟数组处理功能差不多,不深究实现是比较容易理解的。但是后来实际去看了一下源码之后发现,他的实现方式还是非常有趣的,尤其是mapforEach,我们就按照map的流程来看一下,forEach其实差不多,只是没有返回新的节点。

先来看一下流程图:

map流程

当然这么看肯定云里雾里,接下去会对各个函数进行讲解,然后再回过头来配合图片观看更好理解。

开始

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

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

mapforEach的最大区别就是有没有return result

getPooledTraverseContext就是从pool里面找一个对象,releaseTraverseContext会把当前的context对象清空然后放回到pool中。

const POOL_SIZE = 10
const traverseContextPool = []
function getPooledTraverseContext() {
  // args
  if (traverseContextPool.length) {
    const traverseContext = traverseContextPool.pop()
    // set attrs
    return traverseContext
  } else {
    return {
      /* attrs */
    }
  }
}

function releaseTraverseContext(traverseContext) {
  // clear attrs
  if (traverseContextPool.length < POOL_SIZE) {
    traverseContextPool.push(traverseContext)
  }
}

那么按照这个流程来看,是不是pool永远都只有一个值呢,毕竟推出之后操作完了就推入了,这么循环着。答案肯定是否的,这就要讲到React.Children.map的一个特性了,那就是对每个节点的map返回的如果是数组,那么还会继续展开,这是一个递归的过程。接下去我们就来看看。

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

  return traverseAllChildrenImpl(children, '', callback, traverseContext)
}

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

  if (type === 'undefined' || type === 'boolean') {
    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,
      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') {
      // iterator,和array差不多
    } else if (type === 'object') {
      // 提醒不正确的children类型
    }
  }

  return subtreeCount
}

这里就是一层递归了,对于可循环的children,都会重复调用traverseAllChildrenImpl,直到是一个节点的情况,然后调用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,
        keyPrefix +
          (mappedChild.key && (!child || child.key !== mappedChild.key)
            ? escapeUserProvidedKey(mappedChild.key) + '/'
            : '') +
          childKey,
      )
    }
    result.push(mappedChild)
  }
}

mapSingleChildIntoContext这个方法其实就是调用React.Children.map(children, callback)这里的callback,就是我们传入的第二个参数,并得到map之后的结果。注意重点来了,如果map之后的节点还是一个数组,那么再次进入mapIntoWithKeyPrefixInternal,那么这个时候我们就会再次从pool里面去context了,而pool的意义大概也就是在这里了,如果循环嵌套多了,可以减少很多对象创建和gc的损耗。

而如果不是数组并且是一个合规的ReactElement,就触达顶点了,替换一下key就推入result了。

React 这么实现主要是两个目的:

  1. 拆分map出来的数组
  2. 因为对Children的处理一般在render里面,所以会比较频繁,所以设置一个pool减少声明和gc的开销

这就是Children.map的实现,虽然不算什么特别神奇的代码,但是阅读一下还是挺有意思的。

results matching ""

    No results matching ""