# react解析 React.Children(二)
感谢 yck: 剖剖析 React 源码解析 (opens new window),本篇文章是在读完他的文章的基础上,将他的文章进行拆解和加工,加入我自己的一下理解和例子,便于大家理解。觉得yck (opens new window)写的真的很棒 。React 版本为 16.8.6,关于源码的阅读,可以移步到yck react源码解析 (opens new window)
在React实际开发中, React.Children
这个API我们虽然使用的比较少, 但是我们通过这个API可以操作children
, 可以查看文档 (opens new window)
我们来看下这个API的神奇用法
React.Children.map(this.props.children, c => [[c, c]])
下面可以看一下它在项目中的实际用法:
控制台打印渲染的节点和props,如下图 从上图可以得知,通过 c => [[c, c]] 转换以后节点变为了:
// 通过 c => [[c, c]] 转换以后
<div>
<p>1</p>
<p>1</p>
<p>2</p>
<p>2</p>
</div>
2
3
4
5
6
7
我们需要定位到 ReactChildren.js 文件,查看代码 (opens new window), React.Children.map 方法实际就是mapChildren函数,让我们来看看 mapChildren 内部到底是如何实现的吧!
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) {
// 这里是处理 key,不关心也没事
let escapedPrefix = '';
if (prefix != null) {
escapedPrefix = escapeUserProvidedKey(prefix) + '/';
}
const traverseContext = getPooledTraverseContext(
array,
escapedPrefix,
func,
context,
);
traverseAllChildren(children, mapSingleChildIntoContext, traverseContext);
releaseTraverseContext(traverseContext);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
getPooledTraverseContext 和 releaseTraverseContext 中的代码, 引入了对象重用池的概念。这个概念的用处就是维护一个大小固定的对象重用池,每次从这个池子里取一个对象去赋值,用完之后就将对象上的属性清空然后丢回池子。维护这个池子的用意就是提高性能,避免频繁创建销毁多属性对象。
虽然在调用了traverseAllChildren函数,实际调用的是traverseAllChildrenImpl方法。
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;
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') {
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,
);
}
}
}
return subtreeCount;
}
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
这个函数首先 判断 children 的类型, 若children为数组,继续递归调用traverseAllChildrenImpl
,直到处理成单个可渲染的节点,然后调用才能调用callback,也就是mapSingleChildIntoContext
。
最后让我们来读一下 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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mapSingleChildIntoContext
函数其实就是调用React.Children.map(children, callback)
中的callback. 如果map之后还是数组, 那么再次进入mapIntoWithKeyPrefixInternal, 那么这个时候我们就会再次从对象重用池里面去获取context, 而对象重用池的意义也就是在这里, 如果循环嵌套多了, 可以减少很多对象创建和gc的损耗. 如果不是数组, 判断返回值是否是有效的 Element, 验证通过的话就 clone 一份并且替换掉 key, 最后把返回值放入 result 中, result 其实也就是 mapChildren 的返回值.
下面是运行顺序:
mapChildren 函数
|
\|/
mapIntoWithKeyPrefixInternal 函数
|
\|/
traverseAllChildrenImpl函数(循环成单个可渲染的节点,如果不是递归)
|
|单个节点
\|/mapSingleChildIntoContext函数(判断是否是有效Element, 验证通过就 clone 并且替换掉 key,
并值放入result,result就是map的返回值)
2
3
4
5
6
7
8
9
10
11
更多内容:
react解析: React.createElement(一) (opens new window)
react解析: React.Children(二) (opens new window)
react解析: render的FiberRoot(三) (opens new window)
参考:
yck: 剖剖析 React 源码 (opens new window)
Jokcy 的 《React 源码解析》: react.jokcy.me/ (opens new window)
ps: 顺便推一下自己的个人公众号:Yopai,有兴趣的可以关注,每周不定期更新,分享可以增加世界的快乐
男性,武汉工作,会点Web,擅长Javascript.
技术或工作问题交流,可联系微信:1169170165.