Web学习笔记-React(组合Components)

  1. 1. 创建父组件
  2. 2. 从上往下传递数据
  3. 3. 传递子节点
  4. 4. 从下往上调用函数
  5. 5. 兄弟组件间传递消息
  6. 6. 无状态函数组件
  7. 7. 组件的生命周期

本文记录 React 的学习过程,内容为组合 Components。
本节内容是组件与组件之间的组合,例如用不同组件构成 DOM 树,以及给不同的组件传递数据或者调用不同组件的方法,还有不同组件的生命周期。

1. 创建父组件

我们还是继续在之前的 Box 组件上进行操作,首先创建一个 Boxes 组件,其中包含一系列 Box 组件。

components 目录中创建 boxes.jsx

1
2
3
4
5
6
7
8
9
10
11
12
import React, { Component } from 'react';

class Boxes extends Component {
state = { }
render() {
return (
<h1>Boxes</h1>
);
}
}

export default Boxes;

然后修改一下 index.js

1
2
3
4
5
6
7
8
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import 'bootstrap/dist/css/bootstrap.css';
import Boxes from './components/boxes';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Boxes />);

现在我们在 Boxes 中加入多个 Box,当一个组件中包含多个并列元素的时候,需要用一个标签将他们括起来,可以使用 React 中的一个虚拟标签 <React.Fragment>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Component } from 'react';
import Box from './box';

class Boxes extends Component {
state = { }
render() {
return (
<React.Fragment>
<Box />
<Box />
<Box />
</React.Fragment>
);
}
}

export default Boxes;

为了方便也可以用一个数组来表示,将 Box 的信息存到 state 里,由于 React 组件如果有若干个儿子那么他们的 key 需要不一样,因此还需要存一个唯一的 id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { Component } from 'react';
import Box from './box';

class Boxes extends Component {
state = {
boxes: [
{id: 1, x: 0},
{id: 2, x: 0},
{id: 3, x: 0},
{id: 4, x: 0},
]
}
render() {
return (
<React.Fragment>
{this.state.boxes.map(box => (
<Box key={box.id} />
))}
</React.Fragment>
);
}
}

export default Boxes;

2. 从上往下传递数据

通过 this.props 属性可以从上到下传递数据。例如我们在 Boxes 中传递 x

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...

class Boxes extends Component {
state = {
...
}
render() {
return (
<React.Fragment>
{this.state.boxes.map(box => (
<Box key={box.id} x={box.x} name='yyj' />
))}
</React.Fragment>
);
}
}

export default Boxes;

可以在 Box 中输出信息 console.log(this.props); 查看内容。

修改 Box 中的 x

1
2
3
4
5
6
7
8
9
10
11
import React, { Component } from 'react';  // 输入imrc即可补全

class Box extends Component { // 输入cc即可补全
state = {
x: this.props.x,
};

...
}

export default Box;

3. 传递子节点

可以将标签写成 <Box></Box> 的形式,然后在标签中添加子标签:

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
import React, { Component } from 'react';
import Box from './box';

class Boxes extends Component {
state = {
boxes: [
{id: 1, x: 0},
{id: 2, x: 0},
{id: 3, x: 0},
{id: 4, x: 0},
]
}
render() {
return (
<React.Fragment>
{this.state.boxes.map(box => (
<Box key={box.id} x={box.x} name='yyj'>
<h1>Title</h1>
</Box>
))}
</React.Fragment>
);
}
}

export default Boxes;

这样 this.props 中会多一个属性 children,可以使用 [] 单独访问某个子标签。我们可以将这个传过来的值定义在任何地方,例如可以放到每个 Box 组件的最上方:

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
import React, { Component } from 'react';  // 输入imrc即可补全

class Box extends Component { // 输入cc即可补全
state = {
x: this.props.x,
};

handleClickLeft = (step) => {
this.setState({
x: this.state.x - step
});
}

handleClickRight = (step) => {
this.setState({
x: this.state.x + step
});
}

handleClickLeftTmp = () => {
this.handleClickLeft(10);
}

render() { // Component类的函数,用来返回当前组件最后渲染的HTML结构是什么
console.log(this.props);
return (
// HTML标签中可以使用{}写一个表达式
<React.Fragment>
{this.props.children}
<div style={this.getStyles()}>{this.state.x}</div>
<button onClick={this.handleClickLeftTmp} className='btn btn-primary m-2'>Left</button>
<button onClick={() => this.handleClickRight(10)} className='btn btn-success m-2'>Right</button>
</React.Fragment>
);
}

getStyles() {
...
}
}

export default Box;

4. 从下往上调用函数

父元素可以通过 this.props 向子元素传递信息,子元素也可以使用函数向父元素传递信息。假设我们需要实现通过点击删除按钮删除某个 Box,其信息保存在 Boxesstate 中,但是我们点击触发事件是在 Box 中(注意:每个组件的 this.state 只能在组件内部修改,不能在其他组件内修改)。

我们可以在父元素中定义好函数,然后将函数传给子元素:

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
import React, { Component } from 'react';
import Box from './box';

class Boxes extends Component {
state = {
boxes: [
{id: 1, x: 0},
{id: 2, x: 0},
{id: 3, x: 0},
{id: 4, x: 0},
]
}

handleDelete = (boxId) => {
// 遍历一遍state.boxes,将box.id不为传入的参数boxId的数据保留下来
const res = this.state.boxes.filter(box => box.id !== boxId);
this.setState({boxes: res});
}

render() {
return (
<React.Fragment>
{this.state.boxes.map(box => (
<Box key={box.id} id={box.id} x={box.x} name='yyj'
onDelete={this.handleDelete}
/>
))}
</React.Fragment>
);
}
}

export default Boxes;

这样子元素就能调用函数对父元素进行操作了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Component } from 'react';  // 输入imrc即可补全

class Box extends Component { // 输入cc即可补全
...

render() { // Component类的函数,用来返回当前组件最后渲染的HTML结构是什么
console.log(this.props);
return (
// HTML标签中可以使用{}写一个表达式
<React.Fragment>
...
<button onClick={() => this.props.onDelete(this.props.id)} className='btn btn-danger m-2'>Delete</button>
</React.Fragment>
);
}

getStyles() {
...
}
}

export default Box;

现在我们在 Boxes 中实现一个 Reset 按钮实现清空所有 Boxx

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
import React, { Component } from 'react';
import Box from './box';

class Boxes extends Component {
state = {
boxes: [
{id: 1, x: 0},
{id: 2, x: 1},
{id: 3, x: 2},
{id: 4, x: 3},
]
}

handleDelete = (boxId) => {
...
}

handleReset = () => {
const res = this.state.boxes.map(box => {
return {
id: box.id,
x: 0,
}
});
this.setState({boxes: res});
}

render() {
console.log(this.state.boxes);
return (
<React.Fragment>
<button
onClick={this.handleReset}
style={{marginBottom: '15px'}}
className='btn btn-info'
>Reset</button>
{this.state.boxes.map(box => (
<Box key={box.id} id={box.id} x={box.x} name='yyj'
onDelete={this.handleDelete}
/>
))}
</React.Fragment>
);
}
}

export default Boxes;

在控制台观察时可以发现点击 Reset 按钮后 x 确实置零了,但是 Box 显示出来的 x 并没有改变,这是因为 state 值不能在外部修改,因此我们可以将 Box 中的 state 删掉,需要在该组件中渲染外面的 state 的值。

每个维护的数据仅能保存在一个 this.state 中,不要直接修改 this.state 的值,因为 setState 函数可能会将修改覆盖掉。

修改 Boxes,将之前 Box 中操作 state 的函数转移过来:

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
import React, { Component } from 'react';
import Box from './box';

class Boxes extends Component {
state = {
boxes: [
{id: 1, x: 0},
{id: 2, x: 1},
{id: 3, x: 2},
{id: 4, x: 3},
]
}

handleDelete = (boxId) => {
// 遍历一遍state.boxes,将box.id不为传入的参数boxId的数据保留下来
const res = this.state.boxes.filter(box => box.id !== boxId);
this.setState({boxes: res});
}

handleReset = () => {
const res = this.state.boxes.map(box => {
return {
id: box.id,
x: 0,
}
});
this.setState({boxes: res});
}

// 需要知道修改的是哪个box
handleClickLeft = (box) => {
const boxes = [...this.state.boxes]; // 浅拷贝一份
const k = boxes.indexOf(box); // 传入的box是引用,找出其在boxes中的下标k
boxes[k] = {...boxes[k]}; // 再clone一遍,相当于创建新的state,深拷贝
boxes[k].x--;
this.setState({boxes: boxes});
}

handleClickRight = (box) => {
const boxes = [...this.state.boxes];
const k = boxes.indexOf(box);
boxes[k] = {...boxes[k]};
boxes[k].x++;
this.setState({boxes: boxes});
}

render() {
return (
<React.Fragment>
<button
onClick={this.handleReset}
style={{marginBottom: '15px'}}
className='btn btn-info'
>Reset</button>

{this.state.boxes.map(box => (
<Box key={box.id} id={box.id} x={box.x} name='yyj'
onDelete={this.handleDelete}
onClickLeft={() => this.handleClickLeft(box)}
onClickRight={() => this.handleClickRight(box)}
/>
))}
</React.Fragment>
);
}
}

export default Boxes;

然后修改 Box,将 this.state 替换成父组件传递过来的 props

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
import React, { Component } from 'react';  // 输入imrc即可补全

class Box extends Component { // 输入cc即可补全
render() { // Component类的函数,用来返回当前组件最后渲染的HTML结构是什么
return (
// HTML标签中可以使用{}写一个表达式
<React.Fragment>
<div style={this.getStyles()}>{this.props.x}</div>
<button onClick={this.props.onClickLeft} className='btn btn-primary m-2'>Left</button>
<button onClick={this.props.onClickRight} className='btn btn-success m-2'>Right</button>
<button onClick={() => this.props.onDelete(this.props.id)} className='btn btn-danger m-2'>Delete</button>
</React.Fragment>
);
}

getStyles() {
let styles = {
width: '50px',
height: '50px',
backgroundColor: 'lightblue',
color: 'white',
textAlign: 'center',
lineHeight: '50px',
borderRadius: '5px',
position: 'relative',
left: this.props.x
};

if (this.props.x === 0) {
styles.backgroundColor = 'orange';
}

return styles;
}
}

export default Box;

5. 兄弟组件间传递消息

如果组件的结构关系更为复杂,那么就需要将多个组件共用的数据存放到最近公共祖先this.state 中。

我们创建一个 App 组件,其包含两个子组件 NavBar(导航栏)和 Boxes,这两个组件为兄弟组件。

首先是 navbar.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react';

class NavBar extends Component {
state = { }
render() {
return (
<nav className="navbar bg-body-tertiary">
<div className="container-fluid">
<a className="navbar-brand" href="/">Navbar</a>
</div>
</nav>
);
}
}

export default NavBar;

然后是 app.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react';
import NavBar from './navbar';
import Boxes from './boxes';

class App extends Component {
state = { }
render() {
return (
<React.Fragment>
<div className='container'>
<NavBar />
<Boxes />
</div>
</React.Fragment>
);
}
}

export default App;

现在假设我们要在 NavBar 中存放 Boxes 中有几个 Box 的信息,那么只能把信息放到这两个组件的最近公共祖先 App 中。

我们将 Boxes 中与 state 有关的内容都移到 App 中:

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
import React, { Component } from 'react';
import Box from './box';

class Boxes extends Component {
render() {
return (
<React.Fragment>
<button
onClick={this.props.onReset}
style={{marginBottom: '15px'}}
className='btn btn-info'
>Reset</button>

{this.props.boxes.map(box => (
<Box key={box.id} id={box.id} x={box.x} name='yyj'
onDelete={this.props.onDelete}
onClickLeft={() => this.props.onClickLeft(box)}
onClickRight={() => this.props.onClickRight(box)}
/>
))}
</React.Fragment>
);
}
}

export default Boxes;

移动后的 App 如下:

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
import React, { Component } from 'react';
import NavBar from './navbar';
import Boxes from './boxes';

class App extends Component {
state = {
boxes: [
{id: 1, x: 0},
{id: 2, x: 1},
{id: 3, x: 2},
{id: 4, x: 3},
]
}

handleDelete = (boxId) => {
// 遍历一遍state.boxes,将box.id不为传入的参数boxId的数据保留下来
const res = this.state.boxes.filter(box => box.id !== boxId);
this.setState({boxes: res});
}

handleReset = () => {
const res = this.state.boxes.map(box => {
return {
id: box.id,
x: 0,
}
});
this.setState({boxes: res});
}

// 需要知道修改的是哪个box
handleClickLeft = (box) => {
const boxes = [...this.state.boxes]; // 浅拷贝一份
const k = boxes.indexOf(box); // 传入的box是引用,找出其在boxes中的下标k
boxes[k] = {...boxes[k]}; // 再clone一遍,相当于创建新的state,深拷贝
boxes[k].x--;
this.setState({boxes: boxes});
}

handleClickRight = (box) => {
const boxes = [...this.state.boxes];
const k = boxes.indexOf(box);
boxes[k] = {...boxes[k]};
boxes[k].x++;
this.setState({boxes: boxes});
}

render() {
return (
<React.Fragment>
<div className='container'>
<NavBar
boxesCount={this.state.boxes.length} // 将长度传给NavBar
/>
<Boxes
boxes={this.state.boxes}
onReset={this.handleReset}
onClickLeft={this.handleClickLeft}
onClickRight={this.handleClickRight}
onDelete={this.handleDelete}
/>
</div>
</React.Fragment>
);
}
}

export default App;

现在即可在 NavBar 中读取 Boxes 的长度信息了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react';

class NavBar extends Component {
state = { }
render() {
return (
<nav className="navbar bg-body-tertiary">
<div className="container-fluid">
<a className="navbar-brand" href="/">
Navbar <span>Boxes Count: {this.props.boxesCount}</span>
</a>
</div>
</nav>
);
}
}

export default NavBar;

6. 无状态函数组件

当组件中没有用到 this.state 时,可以简写为无状态的函数组件。类相对于函数最大的好处就是可以很方便地维护状态(局部变量)。

无状态函数组件(Stateless Funtion Component),输入 sfc 即可自动补全出来。函数组件相当于只有 render 函数的类组件。注意:函数的传入参数为 props 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from 'react';

const NavBar = (props) => {
return (
<nav className="navbar bg-body-tertiary">
<div className="container-fluid">
<a className="navbar-brand" href="/">
Navbar <span>Boxes Count: {props.boxesCount}</span>
</a>
</div>
</nav>
);
}

export default NavBar;

7. 组件的生命周期

  • Mount 周期(挂载,表示对象被创建出来),执行顺序(按顺序执行三个函数):constructor() -> render() -> componentDidMount()
  • Update 周期(修改,例如点击按钮),执行顺序:render() -> componentDidUpdate()
  • Unmount 周期(删除),执行顺序:componentWillUnmount()

其中,componentDidUpdate 函数有两个参数,分别表示更新前的 propsstate

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
import React, { Component } from 'react';
import NavBar from './navbar';
import Boxes from './boxes';

class App extends Component {
state = {
boxes: [
{id: 1, x: 0},
{id: 2, x: 1},
{id: 3, x: 2},
{id: 4, x: 3},
]
}

componentDidUpdate(prevProps, prevState) {
console.log('App - Update');
console.log(prevProps, this.props);
console.log(prevState, this.state);
}

...

render() {
console.log('App - Render');
return (
...
);
}
}

export default App;

输出的 state 内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{boxes: Array(4)}
boxes: Array(4)
0: {id: 1, x: 0}
1: {id: 2, x: 1}
2: {id: 3, x: 2}
3: {id: 4, x: 3}
length: 4
[[Prototype]]: Array(0)
[[Prototype]]: Object

{boxes: Array(4)}
boxes: Array(4)
0: {id: 1, x: 1} (此处有区别)
1: {id: 2, x: 1}
2: {id: 3, x: 2}
3: {id: 4, x: 3}
length: 4
[[Prototype]]: Array(0)
[[Prototype]]: Object

上一章:Web学习笔记-React(配置环境、ES6语法补充、Components)

下一章:Web学习笔记-React(路由)