React递归生成组件

第一次在业务场景处理递归,并且用递归循环出组件,折腾了一下午成功的遍历了之后总结回顾一下。

什么是递归

递归是一个函数重复调用自己。

递归最普遍的应用就是阶乘,如下:

$$
n! = 12345*…*(n-1)*n
$$

代码实现如下:

1
2
3
4
5
6
7
8
function factorial(num) {
// 边界条件,当值小于0时阶乘无意义,当值为0和1时返回1
if (num <= 1) return 1;
// 将递归的返回的值保存在变量中
const val = num * factorial(num - 1);
console.log(val);
return val;
}

程序运行的结果如下:

1
2
3
4
5
// 当num=1的时候直接返回,因此控制台上没有输出
2;
6;
24;
120;

factorial的调用模式为与输出结果相反,以num=5为例,

1
2
3
4
5
6
7
8
9
10
factorial(5) === 5 * factorial(4);
factorial(4) === 4 * factorial(3);
factorial(3) === 3 * factorial(2);
// 此时factorial(1)已经返回,代码走到 val = num * factorial(num-1) 这一行
// 控制台上输出 val = num * factorial(num-1)的返回值
// 也就是说,val = 2 * 1
// 因此控制台输出2
factorial(2) === 2 * factorial(1);
// 遇到边界条件返回,控制台无输出
factorial(1) === 1;

要理解为什么调用模式和控制台的输出正好相反,就必须要知道在factorial(num)之中,在函数走到console.log(val);之前,已经调用了factorial(num-1),一直到边界条件if (num <= 1)时,才会返回值,走到下一步,在控制台打印出计算后的数值。

递归的边界条件

考虑一下,如果代码没有边界条件:

1
2
3
4
5
6
7
8
function factorial(num) {
// 边界条件,当值小于0时阶乘无意义,当值为0和1时返回1
// if (num <= 1) return 1;
// 将递归的返回的值保存在变量中
const val = num * factorial(num - 1);
console.log(val);
return val;
}

那么函数就会无止境的重复调用自己,陷入无限循环,num的数值会从543210-1-2一直递减,一直到调用栈内存不够:

1
2
3
4
5
6
7
8
9
10
11
RangeError: Maximum call stack size exceeded
at factorial (factorial.js:4:17)
at factorial (factorial.js:4:17)
at factorial (factorial.js:4:17)
at factorial (factorial.js:4:17)
at factorial (factorial.js:4:17)
at factorial (factorial.js:4:17)
at factorial (factorial.js:4:17)
at factorial (factorial.js:4:17)
at factorial (factorial.js:4:17)
at factorial (factorial.js:4:17)

因此,所有的递归函数都需要至少一个边界条件。

React 递归生成组件

以一个 React 的代码文件夹结构为例子:

1
2
3
4
5
6
7
8
9
example
|- public
|- src
|- css
|- js
|- components
|- ui-components
|- containers
|- utils

代码文件夹的数据结构如下:

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
const treeData = [{
name: "example",
children: [
{
name: "public",
children: [],
},
{
name: "src",
children: [
{
name: "css",
children: []
},
{
name: "js",
children: [
{
name: "components",
children: [
{
name: "ui-components",
children: []
}
]
},
{
name: "containers",
children: [],
},
{
name: "utils",
children: []
}
]
},
]
},
]
}]

在不知道文件架有几层的前提之下,想要遍历所有的组件无疑是一件非常困难的事情。就算知道数据的层数,一层层的循环迭代也会影响代码的可读性。鉴于所有的数据结构都是一致的,这种条件下就可以使用递归去循环遍历出结构。

代码如下:

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
// 某处render函数内
{
tree(treeData, 0);
}

// depth并不是必须的,在这里是为了增加一些对padding的优化使得页面看起来结构清晰一些
export const tree = (treeNodes, depth) => {
// 边界条件,如果当前节点不存在则返回空值
if (!treeNodes) {
return null;
}

// javascript map也是遍历列表,但是在遍历完成之后有返回值
return treeNodes.map((node) => {
// 这里可以加一些逻辑处理,或者debugger
const nextDepth = depth + 1;
// 以下是返回的页面,循环调用时React需要一个唯一的key去对页面的更改进行优化
// 使用React.Fragment可以使得页面不产生额外的节点
return (
<React.Fragment key={node.name}>
<div style={{ paddingLeft: depth * 10 }}>{node.name}</div>
{tree(node.children, nextDepth)}
</React.Fragment>
);
});
};

产出效果如下:

1
2
3
4
5
6
7
8
9
example
public
src
css
js
components
ui-components
containers
utils

实际截图:
recursion的实际效果图
其他的用用包括不限于:

  • 假设导航栏数据来自于其他的资源,可以通过其他的 UI 组件,如 bootstrap, antd, material-UI 等动态生成;
  • 通过获取的数据生成列表;
  • 搭建自己的 blog 平台时,如果想要生成更多的子目录的话,也可以用递归去生成文章的结构;