React中setState的使用和源码分析

“ 很早之前看了React中封装setState方法的源码,趁着最近来写一下,帮助大家能更进一步的了解setState方法,并且也能避免在开发过程中出现无形的bug。”

1. setState的使用

在讲解源码之前,我们首先需要先知道它的用法(如果你知道它的用法的话,那么是可以跳过这一部分的,直接到第二段中看源码的实现)。关于setState的用法可以分为三个部分讲解:
(1)setState改变值之后会替换掉初始化的内容吗?
答案是不会的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, {PureComponent} from 'react';
export default class SetStateTest extends PureComponent {
constructor (props) {
super(props);
this.state = {
count: 0,
content: 'hello world',
};
}
// 点击button按钮之后count值被更新了,但是content内容还存在,证明state不是整个替换
handleClick = () => {
this.setState({count: this.state.count + 1});
};
render() {
return (
<div>
<p>count: {this.state.count}</p>
<p>content: {this.state.content}</p>
<button onClick={this.handleClick}>改变count</button>
</div>
);
}
}

(2)为什么页面中state的值改变了,我打印出来的值却还是上一次的?
举一个例子,比如我定义了一个count状态,初始值为0,定义一个button按钮用于改变count的值,这时当我点击button之后页面中count确实改变了,但是在控制台中打印时发现拿到的却是上一次的值。代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, {PureComponent} from 'react';
export default class SetStateTest extends PureComponent {
constructor (props) {
super(props);
this.state = {
count: 0,
};
}
handleClick = () => {
this.setState({count: this.state.count + 1});
console.log('count', this.state.count); // 点击1次按钮,页面显示1,但是在这里控制台打印出的却是上一次的count值0
};
render() {
return (
<div>
<p>count: {this.state.count}</p>
<button onClick={this.handleClick}>改变count</button>
</div>
);
}
}

对于上面,我们如何才能拿到最新更新的值呢?
(1)setState第二个参数是一个回调函数,可以在回调函数中获取;
(2)componentDidUpdate生命周期中也是可以获取到的。
对上面的代码我们做如何的处理即可获取最新的count值,代码如下所示:

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, {PureComponent} from 'react';
export default class SetStateTest extends PureComponent {
constructor (props) {
super(props);
this.state = {
count: 0,
};
}
// 2. 在componentDidUpdate生命周期中可以获取到最新更新的值
componentDidUpdate = () => {
console.log('update count', this.state.count);
};
handleClick = () => {
// 1. 在setState第二个回调函数中可以获取到最新更新值
this.setState({count: this.state.count + 1}, () => {
console.log('callback count', this.state.count);
});
};
render() {
return (
<div>
<p>count: {this.state.count}</p>
<button onClick={this.handleClick}>改变count</button>
</div>
);
}
}

(3)如果有多个setState同时更新count值,那么它会叠加更新吗?
答案是不会的。
比如我在button按钮中写三个setState来更新count,这时我点击button一次,在我们想象中count应该会变为3,但是事实上却是1,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, {PureComponent} from 'react';
export default class SetStateTest extends PureComponent {
constructor (props) {
super(props);
this.state = {
count: 0,
};
}
// 点击1次button按钮最后页面显示count为1,而不是3
handleClick = () => {
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
};
render() {
return (
<div>
<p>count: {this.state.count}</p>
<button onClick={this.handleClick}>改变count</button>
</div>
);
}
}

解决办法:这个时候就会考虑到setState第一个值不为对象,而是函数,函数接收两个参数分别是第一个参数prevState用于获取上一次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
import React, {PureComponent} from 'react';
export default class SetStateTest extends PureComponent {
constructor (props) {
super(props);
this.state = {
count: 0,
};
}
// 改造之后的setState,点击1次button按钮最后页面显示的count3
handleClick = () => {
this.setState((prevState, props) => ({count: prevState.count + 1}));
this.setState((prevState, props) => ({count: prevState.count + 1}));
this.setState((prevState, props) => ({count: prevState.count + 1}));
};
render() {
return (
<div>
<p>count: {this.state.count}</p>
<button onClick={this.handleClick}>改变count</button>
</div>
);
}
}

2. setState的源码解析

提示:如果你在官网下载源码之后,核心的包都在packages文件夹中。这里我只会把关键的代码展示出来,当然如果你感兴趣的话可以找到这几个关键文件夹然后去看看。
首先来解答一下setState改变值之后并不会替换掉初始化的内容,其实原因是因为在源码中使用到了Object.assign()方法,具体源码在react-reconciler--->ReactUpdateQueue.new.js文件中的processUpdateQueue函数下找到getStateFromUpdate函数,它的作用就是拿到state值,然后返回新的state值。重点就在getStateFromUpdate函数中,我们会找到最关键的代码,所以你根本不用担心值会被覆盖的问题。

return Object.assign({}, prevState, partialState);
为什么多个setState同时更新count值,并不会叠加更新。这是因为在源码中我们先找到getStateFromUpdate函数,找到switch中case为UpdateState,此时我们会看到const payload = update.payload;代码,payload其实就是setState传进来要更新的值,当我们在setState中传入对象时,partialState会指向payload,最后会去执行return Object.assign({}, prevState, partialState);其实在这里我们就会发现无论你执行多少次setState,它的partialState都是最初的那个值,并不会根据setState执行多少次而改变。解决:是将setState第一个参数改为函数就可以,在源码中会使用typeof payload判断payload是否是函数,如果是函数会执行如下代码,在这里就可以看到是函数时会执行函数并更新partialState的值,这样就保证了它每一次都是更新后的值,那么多个setState时就可以进行一个叠加更新。

partialState = payload.call(instance, prevState, nextProps);
为什么页面中state的值改变了,我打印出来的值却还是上一次的。这里其实是涉及到了setState异步更新问题。在这篇文章中我先不说明,下一章的时候我会单独写一篇关于setState同步和异步的处理文章,在这里你先记住是如何使用的,下一章节我们继续来探讨setState的用法。