React 类组件

复杂的类组件


那么还是用我们的 +1 功能作为 demo

demo

大家都知道 js 有个 class 关键字,其实就是基于原型链的语法糖

那么类组件的声明就很明显了

我们先写一个展示 n 的功能,后面再来填充

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
n: 0
}
}
render() {
return (
<div>
{this.state.n}
<button onClick={() => alert(this.state.n)}>+1</button>
</div>
)
}
}

上述代码就是一个 react 类组件的基本写法

基本概念

借助上述 demo,我们先来了解一下 react 类组件的基本概念

参数

类组件主要有两种参数

  1. props,来源于父级的传入
  2. state,绑定在 this 上,而不是原型

props

例如,在 index.js 中,对 App 传入参数

1
<App name="myReactApp" />

然后在构造函数中获得 props,并通过父类构造函数初始化 props,之后就可以在 App 中访问 this.props.name 来取得这个参数值

一般不建议在子组件中修改 props,因为给出 props 的值的一定是父组件,所以 props 是属于父组件的,应当只能由父组件来修改,便于代码维护

state

state 是当前 App 的私有变量,不在原型上

一般在构造函数中,通过

1
2
3
this.state = {
...
}

来赋予一个对象

要取值时,也是一样通过 this.state.xxx 来取值

渲染

类组件通过 render 函数来进行渲染,return 一段 JSX 就行

生命周期

类比于 vue 的生命周期,react 也有自己的生命周期

首先是最重要的函数,所有类组件都必须实现的,render 函数!

render 函数负责将组件渲染到页面上

其余常用生命周期钩子函数的对照表如下

react vue
constructor created
componentDidMount mounted
componentDidUpdate updated
componentWillUnmount beforeDestroy

不常用的也给出参考

react vue
shouldComponentUpdate beforeUpdate
UNSAFE_componentWillMount beforeMount
UNSAFE_componentWillUpdate beforeUpdate

可以令 shouldComponentUpdate 函数返回 false 来阻止 render 的执行

更多请看 React.Component 生命周期

魔改

现在修改 onClick,使其具有 +1 的功能

首先想到,写一个 add 方法,然后 +1 行不行

试试看

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 from 'react';
import './App.css';

class App extends React.Component {
constructor(props) {
super(props)
this.state = {
n: 0
}
}
add() {
this.state.n += 1
}
render() {
return (
<div>
{this.state.n}
<button onClick={this.add}>+1</button>
</div>
)
}
}

export default App;

运行一下,发现报错,说找不到 state

为什么呢?因为此处事件绑定的机制,实际上是这样的

假设我们令 onClick=fn,其中 fn 是一个函数

那么经过 webpack 转换后,实际上变成了 onclick = fn.call(window),这时候 add 里的 this 指针就变成了指向全局的了!

那怎么办?容易想到一开始就绑定一个 this

1
<button onClick={this.add.bind(this)}>+1</button>

运行,发现页面没有变化

emmm,让我们加个输出看看

1
2
3
4
add() {
this.state.n += 1
console.log(this.state.n)
}

运行,发现输出是正常的,+1 正常执行了

那么应该就是要我们手动 render 一下

但是用前文说过的那种,也太麻烦了,有没有什么好办法呢

答案是有,react 内置了另一种通知页面更新的办法,那就是 setState

1
2
3
add() {
this.setState({ n: this.state.n + 1 })
}

这样就可以修改 state 中的内容,并且自动通知页面进行更新

现在得到了一个简陋的 +1 demo

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
import React from 'react';
import './App.css';

class App extends React.Component {
constructor(props) {
super(props)
this.state = {
n: 0
}
}
add() {
this.state.n++
this.setState(this.state)
}
render() {
return (
<div>
{this.state.n}
<button onClick={this.add.bind(this)}>+1</button>
</div>
)
}
}

export default App;

接下来一起优化它吧!

优化

this

首先就是这个 onClick,用 bind 也太丑了,有没有好办法呢

刚才说到,实际绑定的 this 会变成全局的 this,所以我们才用 bind 指定了当前对象的 this

那么显然还有一种方法——不支持 this 的箭头函数!

但是现在的 add 写法,是挂载到原型上的,不能写成 add: ()=>{} 这个样子的箭头函数

所以要挂载到对象上,那么容易想到在构造函数中挂载

1
2
3
4
5
6
7
8
9
10
constructor(props) {
super(props)
this.state = {
n: 0
}
this.add = () => {
this.state.n++
this.setState(this.state)
}
}

现在这样,在 button 中就可以不使用 bind 了

1
<button onClick={this.add}>+1</button>

但是这样的话,构造函数未免太长了

于是 ECMA 说好,我再给你个糖,用等号吧

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
import React from 'react';
import './App.css';

class App extends React.Component {
constructor(props) {
super(props)
this.state = {
n: 0
}
}
add = () => {
this.state.n++
this.setState(this.state)
}
render() {
return (
<div>
{this.state.n}
<button onClick={this.add}>+1</button>
</div>
)
}
}

export default App;

该写法完全等价于将 add 在构造函数中声明,都是挂载到对象上的

现在就有了一个看起来很完美的 +1 demo!

对象不变性

但是只是看起来完美,实际上还不够完美

假如我们要后退到某一个历史呢?在这个例子中可以直接 -1,但是要是逻辑复杂了,显然不能直接得到历史结果

那么我们应该尽量使得每次 state 的变化都被保存,便于用户在后退操作中引用

容易想到这样的写法来保存 state

1
2
3
4
5
add = () => {
console.log(this.state)
const n = this.state.n + 1
this.setState({ n })
}

显然,我们用一个新的 state 取代了旧的 state,并且在发生变更前进行了存档

异步更新

现在我们想要看看保存下来的新值是什么

1
2
3
4
5
6
add = () => {
console.log(this.state)
const n = this.state.n + 1
this.setState({ n })
console.log(this.state)
}

发现两次输出竟然是一样的,都是输出了旧的 state!

可是页面明显发生更新了啊,怎么回事

这是因为 setState 的更新是异步的,类似于 setTimeout

所幸,setState 可以接收第二个参数,作为其成功回调

1
2
3
4
5
add = () => {
console.log(this.state)
const n = this.state.n + 1
this.setState({ n }, () => console.log(this.state))
}

现在,打印就正常了

追加更新

+1 demo 不够用啊,复杂一点吧

+2

让我们做一个 +2 功能

容易想到直接 +2 就行,但是我偏要执行两次 +1!

1
2
3
4
add = () => {
this.setState({ n: this.state.n + 1 })
this.setState({ n: this.state.n + 1 })
}

一运行,发现不行,实际上只加了 1!

原因如上文中提到的异步,实际上两次取 this.state.n 都取到了相同的值,所以最后等效于 +1

不过 react 已经考虑到了这个问题——setState 可以接受一个函数作为参数!

setState 如果发现传入的是一个函数,那么会向该函数抛出两个参数,按顺序分别是 this.statethis.props

那么容易得到如下代码

1
2
3
4
add = () => {
this.setState(state => ({ n: state.n + 1 }))
this.setState(state => ({ n: state.n + 1 }))
}

现在就可以正常 +2 了

注意,此处不可以写作

1
() => ({ n: this.state.n + 1})

因为 react 会缓冲更新,此时访问 this.state 并不能第一时间获取最新值

多参数

state 只有一个参数也太惨了,我想多搞点,就多一个 m 吧

假设点击 button 会让 n+=1 且 m 不变

可以得到如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
n: 0,
m: 0
}
}
add = () => {
this.setState(state => ({ n: state.n + 1 }))
}
render() {
return (
<div>
<div>n : {this.state.n}</div>
<div>m : {this.state.m}</div>
<button onClick={this.add}>+1</button>
</div>
)
}
}

这样就实现了上述功能

但是感觉不对啊,setState 不应该是完全替换了 state 吗,可是新的 state 中并没有声明 m 啊,为什么 m 不会变成 undefined 呢

其实 react 在这里会自动比对新旧两个 state 的区别,然后只落实有提及部分,没提及的部分会保持原样

嵌套对象

那我想更新一个嵌套对象,怎么办呢

假设现在 state 中有一个 obj 和一个 n,obj 中有 name 和 age,点击 button 会增加 age 的值

容易得到如下代码

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
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
n: 0,
obj: {
name: 'ringoer',
age: 18
}
}
}
add = () => {
this.setState(state => ({ obj: { age: state.obj.age + 1 } }))
}
render() {
return (
<div>
<div>n : {this.state.n}</div>
<div>obj.name : {this.state.obj.name}</div>
<div>obj.age : {this.state.obj.age}</div>
<button onClick={this.add}>+1</button>
</div>
)
}
}

点击 button 后,发现 name 字段丢失了!

为什么呢?明明 n 字段没有丢失,不是说好 react 会自动对比,保留旧的吗

其实这里的自动保留,只是一个浅拷贝,只能作用于第一层,不能向下深入

这时候一般我们要使用 spread 语法手动展开对象

1
2
3
4
5
6
7
8
add = () => {
this.setState(state => ({
obj: {
...state.obj,
age: state.obj.age + 1
}
}))
}

这样,在 age 变动的时候就不会导致 name 被置空了

忽悠渲染器

不行,渲染器这么智能,我就想让它不智能一把

我如果在 add 的时候,先 +1 再 -1 呢?渲染器这么聪明,应该知道不用渲染吧

1
2
3
4
add = () => {
this.setState(state => ({ n: state.n + 1 }))
this.setState(state => ({ n: state.n - 1 }))
}

答案是不行……渲染器虽然会合并渲染,只渲染一次,但还是发生了渲染

这时候一般有两种办法

可以通过生命周期中的 shouldComponentUpdate 函数,来判断前后 state 是否相等,若相等则不更新

但是太麻烦了,所以就有了懒人方案——改继承!

改成

1
class App extends React.PureComponent

即可实现自动判断的效果

PureComponent 会在 render 之前对比新 state 和旧 state 的每一个 key,以及新 props 和旧 props 的每一个 key

如果所有 key 的值全都一样,就不会 render

如果有任何一个 key 的值不同,就会 render

但是只不过只一个浅对比,深层的话,还是要手写了2333

最终方案

于是我们得到了 +1 demo 的最终解决方案

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 from 'react';
import './App.css';

class App extends React.PureComponent {
constructor(props) {
super(props)
this.state = {
n: 0
}
}
add = () => {
this.setState(state => ({ n: state.n + 1 }))
}
render() {
return (
<div>
{this.state.n}
<button onClick={this.add}>+1</button>
</div>
)
}
}

export default App;

鼓掌!!


感谢阅读

--It's the end.Thanks for your read.--