前言

这是react系列的学习笔记2,介绍一下,React中的一些基本概念,看完后你就可以简单的使用React了

JSX

babel-jsx : https://babeljs.io/docs/en/#jsx-and-react

说到react,就必须要先介绍一下JSX了

JSX是Facebook起草的JS扩展语法,在React中广泛被使用,它的基本语法和HTML差不多,但是又有一些Vue模板的功能,比如下面的例子

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
const span = (<span>我是一个span元素</span>);
// 属性里可以使用JS表达式
const lis = arr.map((item, i) => {
return (<li key={i}>{item}</li>);
});
const obj = {name : "sakura"};
const time = 0;
setInterval(() => {
console.log("重新渲染");
time++;
// 下面就是一个JSX表达式
divElement = (
{/* 这是一个注释 */}
{/* 每个JSX表达式,有且仅有一个根节点,但有时你想并排写多个节点,就要用到React.Fragment */}
{/* <>就是React.Fragment,它不会被渲染, */}
<>
<p>这是段落</p>
{/* 每个JSX元素必须结束,在HTML中,<input>, <br>是允许的,但是JSX中不行 */}
<input type="text"/>
{/* JSX中可以嵌入表达式, 用{包裹起来就可以了} */}
<p>{a} * {b} = {a * b}</p>
{/* 另外null, undefined, false不会被渲染成任何东西 */}
<p>{null}{undefined}{false}</p>
{/* react 元素对象可以被嵌入到JSX中 */}
<p>
{span}
</p>
{/* 数组元素对象可以放置(数组里不能放置普通对象) */}
{[1, 2, 3, 4, undefined, null]}
{/*普通对象不能放置*/}
{/*{obj}*/}
{lis}
{/* class要改为className */}
{/* 表达式可以作为元素属性 */}
<img alt="" className={"img"}
style={{
{/* 表达式里的属性使用小驼峰命名法 */}
width: "400px",
marginRight: "20px"
}} src="https://blog.sakura-snow.com/image/background/index-bg.jpg"/>
{div}
{time}
</>
);
ReactDOM.render(divElement, document.getElementById("root"));
},1000);

是不是非常简单,当然有些规则你记不住也无所谓(毕竟最常用的就那几条),反正到时多报几次错就知道了2333

汇总一下常用的规则如下

  • 每个JSX表达式,有且仅有一个根节点,如果你想平行渲染多个标签,使用<React.Fragment><React.Fragment/>
  • 你可以在JSX中嵌入表达式,类似于JS的模板字符串,你只需要用{}把它包起来就行
    • 数字,字符串,数组可以嵌入并正常显示
    • null、undefined、false,true不会显示
    • 可以放置React元素对象,但不能放置普通对象
  • 表达式可以作为元素属性
  • 表达式里的属性使用小驼峰命名法

这些规则在上面的例子中均有演示,注意一下即可

另外,你还要知道这些JSX是怎么生效的,其实,JSX会被babel编译成React.createElement(所以,你在使用JSX时,一定要import一下React

1
2
3
4
5
6
function createElement<P extends {}>
(
type: FunctionComponent<P> | ComponentClass<P> | string,
props?: Attributes & P | null,
...children: ReactNode[]
) : ReactElement<P>;

image-20210129135250567

你可以在这里自己动手试一下

顺带一提,React.createElement创建出的对象都是不可变的,不能进行任何的修改,只能重新创建

组件属性

组件是一个包含内容、样式和功能的UI单元

React中有两种组件,一种是函数式组件,一种是类组件

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
import React from "react";

// 类组件
class Counter extends React.Component {

constructor(props) {
// super(props)内部调用了this.props = props;
super(props);
}

render() {
return (
<div className={"counter"}>{this.props.number}</div>
)
}
}

// 函数式组件
function FuncCounter(props) {
return (
<div className={"counter"}>
{props.number}
</div>
)
}

export default Counter;
export {FuncCounter}

它们都可以接收外部传来的props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import './App.css';
import Counter, {FuncCounter} from "../Counter/Counter";

function App() {
return (
<div className="App">
<Counter number={1}/>
<FuncCounter number={1}/>
</div>
);
}

export default App;

可以看出

  • 对于函数组件,属性会作为一个对象的属性,传递给函数的参数
  • 对于类组件,属性会作为一个对象的属性,传递给构造函数的参数

另外,根据React单向数据流的思想,子组件无法修改父组件传来的属性

PS:React为Chrome提供了开发者工具

image-20210129143811308

你可以下载他来查看当前的组件树,还有组件的属性和状态

image-20210129143910999

组件状态

另外,一个组件除了父组件传递过来的属性,还要有自身的一些状态(除非这个组件的功能只有渲染)

这个状态就是state

状态初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 状态初始化的方式有两种
class Counter extends React.Component {

// 方法1:直接声明初始化state
state = {
number : this.props.number
}

constructor(props) {
// super(props)内部调用了this.props = props;
super(props);
// 方法2:在构造器中设置state
// this.state = {
// number: 10
// }
}

render() {
return (
<div className={"counter"}>{this.props.number}</div>
)
}
}

可以看到,我们可以用声明或者在构造函数中初始化state,这个state就是组件自生的数据

状态修改

和Vue不一样,如果你修改state中的数据,页面是不会更新的

如果你要在修改state时刷新页面,就要使用setState方法

一旦调用了this.setState,就会导致当前组件重新渲染(所以有时不能直接修改state也不是好事,比如设置timer时可以不用重新)

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
//  Tick.tsx
import React from "react";

interface IProps {
number : number
}

interface IState {
number : number;
timer ?: number;
}

// 类组件
class Tick extends React.Component<IProps, IState> {

// 方法1:直接声明初始化state
// Readonly<IState>确保不能直接修改state里的内容
readonly state : Readonly<IState> = {
number : this.props.number,
timer : undefined
}

constructor(props : any) {
// super(props)内部调用了this.props = props;
super(props);
}

// 这个函数在组件挂载后调用
componentDidMount() {
let timer : any = setInterval(() => {
this.setState({
number : this.state.number + 1
})
}, 1000);
this.setState({
timer
})
}

render() {
return (
<div className={"counter"}>
<span className={"text"}>{this.state.number}</span>
</div>
)
}
}

export default Tick;

对比一下props和state

  • props:该数据是由组件的使用者传递的数据,所有权不属于组件自身,因此组件无法改变该数组
  • state:该数组是由组件自身创建的,所有权属于组件自身,因此组件有权改变该数据

setState的一些问题

其实简单来说就是,setState有两个问题

  • setState对状态的修改,可能是异步执行的(如果改变状态的代码处于某个HTML元素的事件中,则其是异步的,否则是同步)
  • React会对异步的setState进行优化,将多次setState进行合并(将多次状态改变完成后,再统一对state进行改变,然后触发render)

举个例子,假如我们有下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Comp extends React.Component<any, any> {

state = {
num : 0
}

handleClick = () => {
this.setState({
num : this.state.num + 1
})
console.log(this.state.num)
}

render() {
console.log("render")
return (
<>
<p>{this.state.num}</p>
<button onClick={this.handleClick}>点我</button>
</>
)
}
}

如果你按了一下按钮,页面的打印结果如下

image-20210129171502490

可以看到,state实际上没有马上更新

所以,我们要介绍一点新的操作

如果要使用改变之后的状态,需要使用回调函数

这个回调函数会在render后运行

比如下面的代码

1
2
3
4
5
6
7
handleClick = () => {
this.setState({
num : this.state.num + 1
}, () => {
console.log(this.state.num)
})
}

image-20210129172027430

如果新的状态要根据之前的状态进行运算,使用函数的方式改变状态

如果setState的第一个参数不是一个对象而是一个函数,这个函数在执行时会通过参数被传入prevState,也就是之前的状态,而返回值就会和state进行合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
handleClick = () => {
this.setState((prevState : any) => {
return {
num : prevState.num + 1
}
}, () => {
console.log(this.state.num)
})
this.setState((prevState : any) => {
return {
num : prevState.num + 1
}
})
this.setState((prevState : any) => {
return {
num : prevState.num + 1
}
})
}

image-20210129172353480

另外要注意的是,回调函数不是在那次的setState执行后执行的,而是等所有setState执行后,组件render后才执行的

组件事件

在React中,事件有两种,一个是原生事件,一个是自定义事件

原生事件

假如我们要用我们的Tick组件实现倒计时的功能,而且支持暂停,那我们就可以编写下面的代码

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
// 类组件
class Tick extends React.Component<IProps, IState> {

// 方法1:直接声明初始化state
readonly state : Readonly<IState> = {
number : this.props.number,
timer : undefined
}

constructor(props : any) {
// super(props)内部调用了this.props = props;
super(props);
}

// 这个函数在组件挂载后调用
componentDidMount() {
this.resume();
}

// 绑定this为组件实例
parse = () => {
clearInterval(this.state.timer);
}

resume = () => {
this.parse();
let timer : any = setInterval(() => {
this.setState({
number : this.state.number -1
})
}, 1000);
this.setState({
timer
})
}

render() {
return (
<div className={"counter"}>
<span className={"text"}>{this.state.number}</span>
<div>
<button onClick={this.parse}>暂停</button>
</div>
<div>
<button onClick={this.resume}>继续</button>
</div>
</div>
)
}
}

这里我们新增了两个按钮,并且绑定了click事件,如果React检测出这是浏览器支持的原生事件,就会把它绑定到DOM上

并且把它从props中去掉

image-20210129155430876

另外,你要注意一下,事件的this默认指向undefined,如果你想让事件指向组件实例,就要经过特殊的处理,一般的处理方式有两种

  • 使用bind
  • 使用箭头函数

自定义事件

不是原生的事件就都是自定义事件了,假如我们希望组件在倒计时到0时,通知一下父组件,我们就可以使用自定义事件

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
import React from "react";

interface IProps {
number : number,
onOver ?: Function
}

interface IState {
number : number;
timer ?: number;
}

// 类组件
class Tick extends React.Component<IProps, IState> {

// 方法1:直接声明初始化state
readonly state : Readonly<IState> = {
number : this.props.number,
timer : undefined
}

constructor(props : any) {
// super(props)内部调用了this.props = props;
super(props);
console.log(props)
}

// 这个函数在组件挂载后调用
componentDidMount() {
this.resume();
}

// 绑定this为组件实例
parse = () => {
clearInterval(this.state.timer);
}

resume = () => {
this.parse();
if (this.state.number === 0) return;
let timer : any = setInterval(() => {
if (this.state.number === 0) {
clearInterval(timer);
// 调用父组件传递的事件
if (this.props.onOver) {
this.props.onOver();
}
return;
}
this.setState({
number : this.state.number -1
})
}, 1000);
this.setState({
timer
})
}

render() {
return (
<div className={"counter"}>
<span className={"text"}>{this.state.number}</span>
<div>
<button onClick={this.parse}>暂停</button>
</div>
<div>
<button onClick={this.resume}>继续</button>
</div>
</div>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app.tsx
import React from 'react';
import './App.css';
import Tick from "../Tick/Tick";

function App() {
return (
<div className="App">
<Tick number={10} onOver={() => {
console.log("倒计时结束");
}}/>
</div>
);
}

export default App;

可以看到,自定义事件本质上只是一个普通的props属性,我们可以从props中看出来,React实际上只是把它当成了一个函数类型的props属性来处理而已

image-20210129161558284

表单

这里我们先介绍一下受控组件和非受控组件的概念

  • 受控组件:组件的使用者,有能力完全控制该组件的行为和内容。通常情况下,受控组件往往没有自身的状态,其内容完全收到属性的控制。
  • 非受控组件:组件的使用者,没有能力控制该组件的行为和内容,组件的行为和内容完全自行控制。

而表单组件,默认情况下是非受控组件,一旦设置了表单组件的value属性,则其变为受控组件(单选和多选框需要设置checked)

比如说,有下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react";
class FormComp extends React.Component<any, any> {

render() {
return (
<div>
<input/>
</div>
);
}

}

这里渲染出来的就是一个普通的表单组件,它是非受控的,而如果我们稍微修改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from "react";
interface IState {
inputVal : string
}
class FormComp extends React.Component<any, IState> {

state = {
inputVal: ""
}


render() {
return (
<div>
<input value={this.state.inputVal}/>
</div>
);
}

}

这时候,input的内容完全由外部控制,它也就变成了一个受控组件

另外,这时候控制台会报一个错

image-20210129195423019

这是因为,react发现我们没有设置对应的操作来控制它,我们可以设置一个onChange事件

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
interface IState {
inputVal : string
}
class FormComp extends React.Component<any, IState> {

state = {
inputVal: ""
}

updateInputVal(val : string) {
this.setState({
inputVal : val
})
}

render() {
return (
<div>
<input value={this.state.inputVal} onChange={event => {
this.updateInputVal(event.target.value);
}}/>
</div>
);
}

}

image-20210129195959309

这时候input框里的内容可以随着state的值更新而改变了

顺带一提,从上面的报错中我们可以看出

  • 如果只是想给input框设置默认值,应该使用defaultValue
  • 如果是想这个input框的内容始终保持value设置的值,应该多设置一个readOnly属性

知道了这些后,我们就可以做一些非常有趣的效果,比如一个只能输入数字的输入框

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
import React from 'react'

interface IState {
val : string
}
class NumberInput extends React.Component<Object, IState> {

state = {
val: ""
}

updateInputVal = (val : string) => {
val = val.replace(/\D/g, "");
this.setState({
val
})
}
render() {
return (
<input type="text" value={this.state.val}
onChange={e => this.updateInputVal(e.target.value)}
/>
)
}
}

export default NumberInput;

CSS Module

https://blog.sakura-snow.com/post/Blog-webpack-learn-4/

我在写Webpack的博客时写到CSS Module时,本来想在里面演示一下在React或者Vue中使用CSS Module的,因为原生使用实在是太奇怪了,但是没想到怎么通俗易懂的讲清楚,所以就留到现在了

问题不大,我们现在来看一下怎么用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// FormComp.module.scss
.wrapper {
.text {
color: #fff;
font-size: 25px;
}
input {
width: 300px;
height: 30px;
box-sizing: border-box;
border-radius: 5px;
padding: 0 5px;
outline: none;
}
}
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
// FormComp.tsx
import React from "react";
import Style from "./FormComp.module.scss";
interface IState {
inputVal : string
}
class FormComp extends React.Component<any, IState> {

state = {
inputVal: ""
}

updateInputVal(val : string) {
this.setState({
inputVal : val
})
}

render() {
return (
<div className={Style.wrapper}>
<p className={"text"}>{this.state.inputVal}</p>
<input value={this.state.inputVal} onChange={event => {
this.updateInputVal(event.target.value);
}}/>
</div>
);
}
}

应用成功√

这样就不用担心不同组件之间的样式会冲突了

image-20210129202522252