基础-React.Children

React.Children

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

API

React.Children下的API有这些

1
2
3
4
5
6
7
const Children = {
map,
forEach,
count,
toArray,
only,
};

其中map, forEach, count和数组的对应方法功能差不多, 只是在内部多了一些对ReactDOM的特殊处理, toArray方法是将类数组的children转为一个真正的数组, only方法是校验children是否是一个ReactElement, 注意, 是一个.

map流程图

react-children-map

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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返回的如果是数组,那么还会继续展开,这是一个递归的过程。接下去我们就来看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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的开销