ReactChildren
# 目录
React Children 有如下四个方法:
// 操作ReactChildren的方法。ReactChildren不是数组。模拟数组的一些方法。
{
map,
forEach,
count,
toArray,
only,
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# map
map 内部调用 mapChildren
方法。
function mapChildren(children, func, context) {
if (children == null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
return result;
}
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
这个 mapIntoWithKeyPrefixInternal
很有意思,我们来看看。
function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) {
let escapedPrefix = '';
if (prefix != null) {
escapedPrefix = escapeUserProvidedKey(prefix) + '/';
}
// 从缓存池中获取 traverseContext,此时并没有加入 traverseContextPool
const traverseContext = getPooledTraverseContext(
array,
escapedPrefix,
func,
context,
);
// 遍历 children 执行回调,并且将结果加入到 mapResult。
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
// 释放当前遍历的traverseContext。
releaseTraverseContext(traverseContext);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 这里使用了 traverseContext 的缓存池,目的是避免大量的创建对象耗费内存。
- traverseAllChildren 这里将 mapSingleChildIntoContext 抽离出来,便于复用。
- traverseContextPool 里只存未使用的空的 traverseContext,在 releaseTraverseContext 中加入缓存池。
# traverseContext
从缓存池中获取 context:
// 遍历环境缓存池
const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext(
mapResult, // 遍历结果数组
keyPrefix, // traverseContext 的 key
mapFunction, // 遍历回调函数
mapContext, // 遍历的 context
) {
// 如果当前缓存池非空
if (traverseContextPool.length) {
// 取出队尾的traverseContext
const traverseContext = traverseContextPool.pop();
traverseContext.result = mapResult;
traverseContext.keyPrefix = keyPrefix;
traverseContext.func = mapFunction;
traverseContext.context = mapContext;
traverseContext.count = 0;
// 返回修改后的 traverseContext
return traverseContext;
} else {
// 缓存池为空则新建一个 traverseContext,最多 10 个
return {
result: mapResult,
keyPrefix: keyPrefix,
func: mapFunction,
context: mapContext,
count: 0,
};
}
}
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
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
释放 context 到缓存池:
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);
}
}
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
- 这种写法在需要频繁创建对象的场景中可以参考。缓存池大小 POOL_SIZE 需要权衡考虑效率和内存问题。如果 POOL_SIZE 太小,就不能很好的起到缓存的效果,如果太大缓存池本身就需要占用太多内存,而且用不完的 context 对象也容易造成浪费和低效。
# traverseAllChildren
traverseAllChildren 内部由 traverseAllChildrenImpl 实现,主要作用是遍历目标 children,调用 callback,维护 children 的 key 值。
// 返回子代数量
function traverseAllChildrenImpl(
children, // 遍历目标
nameSoFar,
callback, // mapSingleChildIntoContext 内部的遍历回调器
traverseContext,
) {
const type = typeof children;
if (type === 'undefined' || type === 'boolean') {
// All of the above are perceived as null.
children = null;
}
// 为 true 表示不需要进一步处理,可以直接 callback。(null,string,number,Element,Portal)。
// 因为只有一个元素,只 callback 一次。
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 初始化,children 不是数组,获取 key 值
nameSoFar === '' ? SEPARATOR + getComponentKey(children, 0) : nameSoFar,
);
return 1;
}
let child;
let nextName;
let subtreeCount = 0; // Count of children found in the current subtree.
// 如:.j:
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 {
// 针对不是数组但内部实现了迭代器的 children。
const iteratorFn = getIteratorFn(children);
if (typeof iteratorFn === 'function') {
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') {
// 如果传入 children 是对象,则报错。
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;
}
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
- traverseAllChildrenImpl 如果发现 children 是数组则会递归遍历,最终将 children 展平(包括多为数组),执行 callback。 traverseAllChildrenImpl 只回调 children 的叶子节点。
# mapSingleChildIntoContext
map 所使用的 contextMap 是 mapSingleChildIntoContext,这里才真正调用用户传入的回调,并且返回处理后的节点。
function mapSingleChildIntoContext(bookKeeping, child, childKey) {
const {result, keyPrefix, func, context} = bookKeeping;
// 调用用户的回调函数
let mappedChild = func.call(context, child, bookKeeping.count++);
if (Array.isArray(mappedChild)) {
// 如果返回了数组,继续进行 map
mapIntoWithKeyPrefixInternal(mappedChild, result, childKey, c => c);
} else if (mappedChild != null) {
// 是否是 ReactElement
if (isValidElement(mappedChild)) {
// 处理mappedChild的 key 值
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数组
result.push(mappedChild);
}
}
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
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
- 这里在回调时,如果用户传回来的还是数组,就继续 map ,只有 用户回调的不是数组且为合法的 ReactElement 时,才会被放入 result 中。result 是 map 的返回值。
# ChildrenKey 的维护
key 所使用的分隔符:
const SEPARATOR = '.';
const SUBSEPARATOR = ':';
1
2
2
生成 key 值的算法:
function getComponentKey(component, index) {
// Do some typechecking here since we call this blindly. We want to ensure
// that we don't block potential future ES APIs.
// 如果组件有 key 值则使用
if (
typeof component === 'object' &&
component !== null &&
component.key != null
) {
// Explicit key
return escape(component.key);
}
// Implicit key determined by the index in the set
// 使用 36 进制,即 0-9-a-z。(35).toString(36) === 'z'。
return index.toString(36);
}
function escape(key) {
const escapeRegex = /[=:]/g;
const escaperLookup = {
'=': '=0',
':': '=2',
};
const escapedString = ('' + key).replace(escapeRegex, function(match) {
return escaperLookup[match];
});
return '$' + escapedString;
}
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
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
- 因为 key 中使用了固定的分隔符,所以用户传递的 key 需要 escape 做等意替换,并且添加前缀
$
。
key 值的命名方法:
.key => .key:key1 => .key:key1:key2 ...
1
# forEach
forEach 内部由 forEachChildren
实现。代码如下:
function forEachChildren(children, forEachFunc, forEachContext) {
if (children == null) {
return children;
}
const traverseContext = getPooledTraverseContext(
null,
null,
forEachFunc,
forEachContext,
);
traverseAllChildren(children, forEachSingleChild, traverseContext);
releaseTraverseContext(traverseContext);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
由此可见:
- forEach 相比于 mapIntoWithKeyPrefixInternal,只是 contextMap 修改成了
forEachSingleChild
,其他代码并未变化。 - forEach 和 map 的区别是:forEach 没有返回值;不接受用户回调的结果。
forEachSingleChild 的处理也很简单,只是调用了回调:
function forEachSingleChild(bookKeeping, child, name) {
const {func, context} = bookKeeping;
func.call(context, child, bookKeeping.count++);
}
1
2
3
4
2
3
4
# count
count 内部由 countChildren 实现,主要作用是返回拉平后的 children 的叶子节点的数量。
function countChildren(children) {
return traverseAllChildren(children, () => null, null);
}
1
2
3
2
3
# toArray
toArray 将 children 以数组形式返回。这里内部不需要执行回调,因此 contextMap 为 null,
function toArray(children) {
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, child => child);
return result;
}
1
2
3
4
5
2
3
4
5
# only
only 内部由 onlyChild
实现。only 验证 children 是否是单节点,并将之返回。代码如下:
function onlyChild(children) {
invariant(
isValidElement(children),
'React.Children.only expected to receive a single React element child.',
);
return children;
}
1
2
3
4
5
6
7
2
3
4
5
6
7
编辑 (opens new window)
上次更新: 2022/04/15, 00:23:56