《大前端三问》 -- React组件入门

1 组件

1.1 定义React组件

定义一个React组件-Welcome.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import PropType from 'prop-types';
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = { username: props.username};
}
render() {
return (
<h1>Hello, {this.props.username}!</h1>
);
}
}
export default Welcome;

1.2 无状态函数组件

无状态函数组件-Star.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import PropType from 'prop-types';
import './star.css';
// 默认入参相当于defaultProps,但是如果设置了defaultProps优先级更高
// PS:更建议使用defaultProps
const Star = ({ selected = true, onClick = f => f }) => {
return (
<div className={(selected ? 'star selected' : 'star unselected')}
onClick={onClick}>
</div>
);
};
Star.propType = {
selected: PropType.bool,
onClick: PropType.func
}
export default Star;
  • 组件可以组合;
  • 可以导入CSS或图片资源(基于Webpack);

2 组件的生命周期

2.1 State 的更新可能是异步的

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。所以,下面这段代码可能会出现意料之外的问题

下面这段代码可能会出现意料之外的问题
1
2
3
4
// 因为 setState() 可能会异步调用,所以this.state的值不能确定
this.setState({
counter: this.state.counter + this.props.increment,
});

所以,如果要基于现在有state来改变state状态,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:

解决上面setState()异步调用导致的问题
1
2
3
this.setState((state, props) => ({
counter: state.counter + props.increment
}));

3 组件的设计原则

3.1 单向数据流

如果你把一个以组件构成的树想象成一个 props 的数据瀑布的话,那么每一个组件的 state 就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。

3.2 尽量使用无状态函数组件

3.3 使用class-fields语法

像下面的Button当点击时,其回调函数handleClick中的this有可能会改变。因为在JS中,一般函数中的this指的是调用此方法的对象,因为Button的回调是系统回调,所以此this有可能会变成全局window对象。

没有绑定this的有问题用法
1
2
3
4
5
6
7
8
9
handleClick() {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
<Button onClick={this.handleClick}>
Click me
</Button>

为了解决此问题,我们一般会使用键头函数:

使用键头函数作为回调函数,解决this指向问题
1
2
3
<Button onClick={(e) => this.handleClick(e)}>
Click me
</Button>

使用键头函数可以保证函数中的this指向其外层定义对象。但是这样有个问题,就是如果回调方法为props传入的话,每次渲染Button控件时都会创建不同的回调函数,此时组件会造成额外的开销。

所以,回调函数使用class fields语法会是更好的选择,其可以解决this引用问题,也可以不需要在控件重绘时再重复生成回调函数。用法如下:

定义回调函数使用class-fields语法
1
2
3
4
5
6
7
8
9
10
11
// 定义回调函数使用class-fields语法
// 其实相当于指键头函数的定义放在了函数定义阶段
handleClick = (event) => (event) {
this.setState(state => ({
isToggleOn: !state.isToggleOn
}));
}
<Button onClick={this.handleClick}>
Click me
</Button>
  • 但是如果需要向函数传递参数,还是建议在指定回调时使用键头函数
使用键头函数传递参数
1
2
3
4
5
6
7
handleClick(e, userId) { // 这里在自身函数中再调用传入的回调函数
this.props.handleClick && this.props.handleClick(userId)
}
<Button onClick={(e) => this.handleClick(e, userId)}>
Click me
</Button>

4 React.Children组件子元素

组件有一个特殊的this.props.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
// StartBox.js
// children 为 props的特有属性,表示组件中的子元素
const StarBox = ({ isShow = true, children }) => {
return (
<div>
{
isShow? children : null
}
</div>
);
};
// app.js
class App extends React.Component {
render() {
return (
<StarBox isShow={false}>
<Star/>
<Star/>
<Star/>
</StarBox>
)
}
}

App.js中的isShow={false}时,其组件中定义的3个Star子组件都不会显示。因为在StarBox组件中作了判断,isShow为false时,并没有输出children,而这个children就是在外层父组件中传入的3个Star元素。这里的children是个数组。

4.1 校验单个子元素

在有些场合你可以使用

使用React.Childre校验单个子元素
1
React.Children.only(this.props.children);

方法来校验单个子元素。也就是说,如果像上例中输出子元素是一个数组,则会报错

校验非单个子元素会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// StartBox.js
// children 为 props的特有属性,表示组件中的子元素
const StarBox = ( props ) => {
return (
<div>
{ React.Children.only(props.children) }
</div>
);
};
// app.js
class App extends React.Component {
render() {
return (
<StarBox>
<Star/> {/* 这里注意了,只能是一个Star了,如果有多个子元素children会变成数组,从而不能通过Children.only校验 */}
</StarBox>
)
}
}

但是上面的例子也要注意的就是Star自身可以是复杂的组合组件,这是没问题的。

5 Fragments

我们知道,React的渲染方式为在render()方法中返回声明式元素,如:

return只能返回单个root元素
1
2
3
4
5
6
7
8
9
10
11
class Table extends React.Component {
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
}

即在return时,只能返回单个Root元素,如果上面的Columns组件这样返回,将会报错:

1
2
3
4
5
6
7
8
9
10
class Columns extends React.Component {
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
}

6 Context

Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据。一般来说,React推崇的是自顶向下的单方向数据流,数据从root component利用props属性呈树型地往下传递。而Contex则看起来与此理念相悖。但是在有些场合,这种“全局”变量又显得那么重要,比如要给每个控件增加一个全局的主题属性,如果没有全局变量的话,这将变得非常复杂(当然,也可以使用redux这种单一数据源的框架来解决)。

6.1 使用Context全局变量

6.1.1 React.createContext

我们就用上面说的设置全局主题的示例。我们首先应该要创建一个Context对象,使用React.createContext(defaultValue)api:

ThemeContext全局对象
1
const ThemeContext = React.createContext( themes.dark /* 默认值 */);

然后这里涉及2个问题:一个是全局对象的读取,另一个是全局对象的修改。一种简单的操作是全局对象只放在顶层容器组件中可修改,其值与顶层容器组件的state绑定。

6.1.2 Context.Provider

容器组件Context.Provider<Context.Provider value={this.state.value}>可以向下传递Context全局对象,同时,让此Context值与state绑定。

顶层容器向下传递Context对象及state绑定context值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constructor(props) {
// ...
this.state = {
ThemeConst.Light,
};
}
_toggleTheme = () => {
this.setState(state => ({
theme: (state.theme === ThemeConst.Dark)? ThemeConst.Light: ThemeConst.Dark,
}));
}
<ThemeContext.Provider value={this.state.theme}>
<ThemedButton onClick={this._toggleTheme}> Click to change theme </ThemedButton>
</ThemeContext.Provider>

在上面的代码示例中,context传递给了ThemeButton,那在展示组件中怎样将其值取出呢?

6.1.3 contextType

我们看看此button的代码。在ThemedButton中,我们可以直接利用this.context取出全局变量,从而决定自己的展示。

在底层展示组件中直接利用this.context取出全局变量
1
2
3
4
5
6
7
8
9
class ThemedButton extends React.Component {
render() {
let theme = this.context;
return (
<button {...this.props} style={{backgroundColor: theme.Background}} />
);
}
}
ThemedButton.contextType = ThemeContext;

6.2 子组件中修改全局变量

在上面的示例中,子组件只可以被动地接受由Context.provider传下来的全局变量。那子组件是否有方法可以修改全局变量的值呢?

答案是有的,就是利用Context.Consumer

首先我们在定义Context时将之定义为一个对象,其中包含一个可以改变自身值的函数toggleTheme:

定义Context对象,包含一个可以改变自身值的回调函数toggleTheme
1
2
3
4
const ThemeContext = React.createContext({
theme: ThemeConst.Dark, // 默认值
toggleTheme: () => {}
});

这个回调固然是要在根容器组件中定义:

根容器组件中定义
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
constructor(props) {
// ... 省
this.state = { // 与全局对象一样,此State将用于绑定全局Context。回调函数也在此定义
theme: ThemeConst.Light,
toggleTheme: this._toggleTheme
};
}
// 改变Context全局变量的回调函数定义。
// 注意:这里改变的是State,因为State已与Context绑定了
_toggleTheme = () => {
this.setState(state => ({
theme: state.theme === ThemeConst.Dark? ThemeConst.Light: ThemeConst.Dark
}));
}
render() {
return (
<div>
<ThemeContext.Provider value={this.state}>
<ThemeToggleButton> Click to change theme </ThemeToggleButton>
</ThemeContext.Provider>
</div>
)
}

6.2.1 ThemeContext.Consumer

在子展示组件中,使用ThemeContext.Consumer订阅了Context的修改。其内包含一个回调,当Context改变时此订阅将会得到通知,子组件也将得到刷新

ThemeToggleButton使用ThemeContext.Consumer与全局Context绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ThemeToggleButton extends React.Component {
render() {
return (
<ThemeContext.Consumer>
{
(ThemeContext) => (
<button
onClick={ThemeContext.toggleTheme}
style={{ backgroundColor: ThemeContext.theme.background }}>
{this.props.children}
</button>
)
}
</ThemeContext.Consumer>
);
}
}

可以想象,这一套和Redux的单一数据流很像,没错,Redux就是基于Context这一套实现滴~

7 小结

1
2

8 引用

  1. 《谈一谈 Normalize.css》- 会飞的贼xmy
坚持原创技术分享,您的支持将鼓励我继续创作!