简析React源码之ReactChildren.js

前言

近来一直在研究React源码以及实现的原理,这一章的内容主要是分析ReactChildren.js的代码,以及实现的功能。本文的代码是基于Reactv16.6.3版本,详细的代码React源码可以去github下载,同时本章的示例代码可以通过我的github下载学习示例代码。本章主要是简析React源码中的ReactChildren.js文件,原始代码请参考react/src/ReactChildren.js文件,本章只是针对功能函数进行分析,代码可能略有缺失,完整代码请参考源码。

演示Demo

Block
1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
function ReactChildrenDemo(props){
conosle.log(props.children);
console.log(React.Children.map(props.children),child=>[child,[child,child]])
return props.children
}

export default ()=>(
<ReactChildrenDemo>
<span>demo1</span>
<span>demo1</span>
</ReactChildrenDemo>
)
示例模块

显示效果图

<图1> Rmap函数流程图

代码分析

本章主要以React.Children提供的map函数来分析ReactChildren.js文件。

示例demo代码

Block
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 示例demo代码 -->
import React from 'react';
function ReactChildrenDemo(props){
conosle.log(props.children);
console.log(React.Children.map(props.children),c=>[c,[c,c]])
return props.children
}
export default ()=>(
<ReactChildrenDemo>
<span>demo1</span>
<span>demo1</span>
</ReactChildrenDemo>
)
Block
1
2
3
4
5
6
7
8
<!-- ReactChildren.js -->
export {
forEachChildren as forEach,
mapChildren as map,
countChildren as count,
onlyChild as only,
toArray,
};
  • React.Children提供的map函数很容易让人联想到Array提供的map函数,但是两者实质上并不相同,入参以及返回的数据也不同。这里以Rmap代称呼React.Children提供的map函数,Rmap入参需要三个 props.children、遍历的traverse函数、执行Rmap操作的环境context。

mapChildren函数

Block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- mapChildren函数 -->

/**
* map遍历的children一定要为`props.children`
*
*
* @param {?*} children 传递进来的props.children.
* @param {function(*, int)} func 为传递进来的遍历函数.
* @param {*} context Rmap函数执行的上下文.
* @return {object} 函数执行返回的结果
*/

// 实际的map函数
function mapChildren(children, func, context) {
if (children === null) {
return children;
}
const result = [];
mapIntoWithKeyPrefixInternal(children, result, null, func, context);
}
  • 这里需要注意的是props.children可能存在三种值;

    • 如果当前组件没有子节点,它就是undefined;
    • 如果有一个子节点,数据类型是Object;
    • 如果有多个子节点,数据类型就是array(Ps:通过typeof显示仍然为object);
  • 只有传递进来的children不为null的时候才会进入到mapIntoWithKeyPrefixInternal函数内

mapIntoWithKeyPrefixInternal函数

Block
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- mapIntoWithKeyPrefixInternal函数 -->

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);
// 清除对象的属性,如果没超过对象池则在对象池中添加该对象
}

getPooledTraverseContext函数

Block
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
<!-- getPooledTraverseContext函数 -->

const traverseContextPool = [];
const POOL_SIZE = 10;
function getPooledTraverseContext(
mapResult,
keyPrefix,
mapFunction,
mapContext,
) {
if (traverseContextPool.length) {
const 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,
};
}
}

releaseTraverseContext函数

Block
1
2
3
4
5
6
7
8
9
10
11
12
<!-- releaseTraverseContext函数 -->

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);
}
}
  • 这里主要是负责处理key相关以及遍历children的操作,同时这里应用了一个对象池的概念。
    • 对象池:简单点说就是因为js如果大量的创建对象,使用完后再销毁对象,其实是一件非常影响性能的事情,所以引入一个对象池的概念,在对象池中创建一定的对象,每次使用完毕后讲对象变成未激活状态来代替销毁对象,这样能够保持浏览器的性能,同时也是避免大量对象的创建以及销毁带来的内存抖动。
  • 这里的getPooledTraverseContext函数就是这样的一个负责从对象池中获取对象的函数。
    • 如果在执行时判定如果对象池traverseContextPool为空则会创建一个新的对象并且返回。
    • 如果在执行时判定如果对象池traverseContextPool不为空,则会取出最后一个对象并且返回。
  • 当执行完traverseAllChildren这个真正跟Rmap相关的函数以后就会执行releaseTraverseContext函数,该函数主要负责的就是将对象清空也就是变成为激活状态,并且会判定当前对象池中是否已经超过了设定的最大容量如果没有则会向对象池中塞入该未激活状态的对象。

traverseAllChildren(traverseAllChildrenImpl)函数

Block
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<!-- traverseAllChildren函数 -->
/**
*
*
* @param {*} children 传递的props.children
* @param {*} callback 为传递进来的mapSingleChildIntoContext函数
* @param {*} traverseContext 从对象池中获取的对象,也是执行的上下文
* @returns
*/
function traverseAllChildren(children, callback, traverseContext) {
if (children == null) {
return 0;
}
return traverseAllChildrenImpl(children, '', callback, traverseContext);
}

<!-- traverseAllChildrenImpl函数 -->

/**
*
*
* @param {*} children 初始值 props.children,后续为递归传递的子节点
* @param {*} nameSoFar 初始值 ''
* @param {*} callback mapSingleChildIntoContext
* @param {*} traverseContext 这里的context为对象池中获取的对象
*/
function traverseAllChildrenImpl(
children,
nameSoFar,
callback,
traverseContext,
) {
const type = typeof children;
let invokeCallback = false;

if (type === "undefined" || type === "boolean") {
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,
);
}
} else if (type === 'object') {
let addendum = '';
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;
}

const POOL_SIZE = 10;
const traverseContextPool = [];
function getPooledTraverseContext(
mapResult,
keyPrefix,
mapFunction,
mapContext,
) {
if (traverseContextPool.length) {
const 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,
};
}
}
  • traverseAllChildren函数并没有多大意义,主要执行的函数是traverseAllChildrenImpl函数。
  • traverseAllChildrenImpl接收四个参数。
    • children 初始值 props.children,后续为递归传递的子节点
    • nameSoFar 初始值 ‘’ key相关
    • callback 初始值为 mapSingleChildIntoContext
    • traverseContext 为对象池中获取的对象
  • 只有当传递的children为数组时,会进行遍历操作,生成新的namePrefix以及subtreeCount进行计数(key相关)。同时在调用自身(traverseAllChildrenImpl)函数执行一个大的递归操作,只有当传递进函数的children为单节点同时为React可以解析的数据类型(undefined、boolean、string、number以及object)时才会执行callback函数也就是mapSingleChildIntoContext函数

mapSingleChildIntoContext函数

Block
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
<!-- mapSingleChildIntoContext函数 -->
/**
*
*
* @param {*} bookKeeping 为poolContext中获取的对象
* @param {*} child 传递的单节点
* @param {*} childKey 节点key
*/
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,
// 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);
}
}
  • mapSingleChildIntoContext接受三个参数bookKeeping(为poolContext中获取的对象)、children为props.Children获取的单一节点、childrenKey为key相关。
  • mappedChild为Rmp传递进的递归函数生成的数据,如果mappedChild生成的数据为数组则会再调用mapIntoWithKeyPrefixInternal函数执行递归操作,如果不是则会判定是否为React元素如果为React元素则会克隆元素并且塞入result中(引用传递)

forEach、count、only以及toArray

  • forEach与toArray跟map函数大体相同只是调用的callback函数不同,利用引用传递同时有无返回数据的不同
  • only 判定是否为单一节点
  • count 判定子节点的个数

具体API的解析可以参考ReactApi文档