React(4-1):状态

ObjectKaz Lv4

组件的状态

使用状态

要使用状态,需要:

  1. 函数组件 改成 类组件
  2. 定义构造函数(参数为 props),在构造函数中定义初始状态。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
class Counter extends React.Component {
constructor(props)
{
super(props)
this.state = {count: 0}
}

render() {
return (<div><p>{this.state.count}</p></div>);
}
}

更新状态

可能很多人会这么认为,更新状态就是直接修改 this.state

但别忘了,react 元素是不可变的,光修改 this.state ,状态是改了,但不会触发 UI 更新。

有一个例外:在 constructor 里可以直接给 this.state 赋值

为了解决这个问题,React 提供了 this.setState(状态对象[, 回调函数]) 来修改状态。

  • 状态对象里,不必包含全部的属性,因为修改后的状态会与原状态合并。
  • 回调函数则会在UI 刷新后调用,但通常并不会用到。

不要直接修改状态,否则不会更新视图,唯一可以直接修改的是构造函数

1
2
3
4
5
6
7
8
9
10
11
12
class Counter extends React.Component {
constructor(props)
{
super(props)
this.state = {count: 0}
}

render() {
let handleClick = () => this.setState({count: this.state.count + 1})
return (<div><button onClick={handleClick}>+1</button><p>{this.state.count}</p></div>);
}
}

实际上,上面的例子存在着一些问题,我们会在接下来的一节中提到。

使用函数作为参数

为了保证性能,React 可能会将多个 setState() 的调用合并成一个。

这意味着, state 中的值可能只会在合并后才会更新。而在之前的调用中,它可能并不触发更新。

我们来看一下这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Counter extends React.Component {
constructor(props)
{
super(props)
this.state = {count: 0}
}

render() {
let handleClick = () => {
this.setState({count: this.state.count + 1})
this.setState({count: this.state.count + 1})
}
return (<div><button onClick={handleClick}>+1</button><p>{this.state.count}</p></div>);
}
}

我们在这里连续调用了两次 setState,但是,当你打开浏览器后,你会发现,每次它只自增了一次!

我们不妨将第二个 setState 替换成 console.log 康康:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Counter extends React.Component {
constructor(props)
{
super(props)
this.state = {count: 0}
}

render() {
let handleClick = () => {
this.setState({count: this.state.count + 1})
console.log(this.state.count)
}
return (<div><button onClick={handleClick}>+1</button><p>{this.state.count}</p></div>);
}
}

我们发现,执行 setState 之后,this.state.count 并没有立即更新。

这印证了这节开头的那句话:React 可能会将多个 setState() 的调用合并成一个。

那怎么拿到最新的状态呢?React 还有一个操作, 就是在 setState() 中传入一个函数:

  • 第一个参数是 state
  • 第二个参数是 props

我们可以修改一下这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Counter extends React.Component {
constructor(props)
{
super(props)
this.state = {count: 0}
}

render() {
let handleClick = () => {
this.setState((state, props) => ({count: state.count + 1}))
this.setState((state, props) => ({count: state.count + 1}))
}
return (<div><button onClick={handleClick}>+1</button><p>{this.state.count}</p></div>);
}
}

这下,就可以每次自增两次了!

同步还是异步

接下来,我们来康康 setState 到底是同步的还是异步的。

第一种情况,在绑定的事件中,它的调用是异步的,从上一节的例子便可看出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Counter extends React.Component {
constructor(props)
{
super(props)
this.state = {count: 0}
}

render() {
let handleClick = () => {
console.log(this.state.count)
this.setState({count: this.state.count + 1})
console.log(this.state.count)
}
return (<div><button onClick={handleClick}>+1</button><p>{this.state.count}</p></div>);
}
}

如果我希望状态延迟2秒更新,那么我们加上 setTimeout 康康:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Counter extends React.Component {
constructor(props)
{
super(props)
this.state = {count: 0}
}

render() {
let handleClick = () => {
setTimeout(() =>{
console.log(this.state.count)
this.setState({count: this.state.count + 1})
console.log(this.state.count)
}, 2000)
}
return (<div><button onClick={handleClick}>+1</button><p>{this.state.count}</p></div>);
}
}

对比前后控制台的输出,你惊奇的发现,它立马更新了!这似乎是同步的。

如果我们使用异步函数,康康:

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
class ApiList extends React.Component {
constructor(props) {
super(props);
this.state = { list: [] };
}

render() {
let handleClick = async () => {
let res = await fetch("https://api.github.com");
let data = await res.json();
console.log(this.state.list);
this.setState({
list: Object.entries(data),
});
console.log(this.state.list);
};
return (
<div>
<button onClick={handleClick}>加载数据</button>
<ul>
{this.state.list.map((x, i) => (
<li key={i}>
{x[0]}:{x[1]}
</li>
))}
</ul>
</div>
);
}
}

我们发现在异步执行的函数里,它的更新也是同步的。

哪怕我们再加了一句 setState 语句:

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
class ApiList extends React.Component {
constructor(props) {
super(props);
this.state = { list: [] };
}

render() {
let handleClick = async () => {
console.log(this.state.list);
this.setState({
list: [],
});
console.log(this.state.list);
let res = await fetch("https://api.github.com");
let data = await res.json();
console.log(this.state.list);
this.setState({
list: Object.entries(data),
});
console.log(this.state.list);
};
return (
<div>
<button onClick={handleClick}>加载数据</button>
<ul>
{this.state.list.map((x, i) => (
<li key={i}>
{x[0]}:{x[1]}
</li>
))}
</ul>
</div>
);
}
}

它也是同步的。

需要注意一点,如果async函数内没有异步执行的函数,那么它便是异步执行的:

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
class ApiList extends React.Component {
constructor(props) {
super(props);
this.state = { list: [] };
}

render() {
let handleClick = async () => {
console.log(this.state.list);
this.setState({
list: [],
});
console.log(this.state.list);
// let res = await fetch("https://api.github.com");
// let data = await res.json();
let data = [["current_user_url", "https://api.github.com/user"]];
console.log(this.state.list);
this.setState({
list: Object.entries(data),
});
console.log(this.state.list);
};
return (
<div>
<button onClick={handleClick}>加载数据</button>
<ul>
{this.state.list.map((x, i) => (
<li key={i}>
{x[0]}:{x[1]}
</li>
))}
</ul>
</div>
);
}
}

此外,对于原生绑定的事件,它也是同步的;而对于组件的生命周期函数,它是异步的。这里就不详细探讨了。

小结一下:

  • 同步的情况:异步函数(里面有异步执行的代码)、setTimeout、原生事件
  • 异步的情况:事件、生命周期函数

React 的事务机制

上面的结论可能看起来令人难以理解。下面我们来探讨一下这些结论背后的一系列机制。

前面提到,React 元素是不可变的,只有 render 函数被触发时,元素才会更新。

所以,React 提供了 setState 函数,使得数据修改时,UI 会自动更新。

但是,元素的更新可不是一件简单的事啊,在更新时,它得遍历 DOM 树,得逐一比较,更新。对于规模比较大的网页,这无疑是十分影响性能的。

也就是说,如果一调用 setState 就更新UI,那么当它被频繁调用时,便会影响性能。

为了提高性能,大部分情况下(例如生命周期函数和事件调用),setState 函数都会通过 事务机制 来触发更新。

当事务开启时,所有通过 setState 改变的状态都会临时保存在一个队列里,而事务关闭时,React 便会批量更新 UI

当生命周期函数或事件调用开始时,React 便会开启一次事务。当声明周期函数调用或事件调用结束时,React 就会关闭事务。

我们看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Counter extends React.Component {
constructor(props)
{
super(props)
this.state = {count: 0}
}

render() {
let handleClick = () => {
this.setState((state, props) => ({count: state.count + 1}))
this.setState((state, props) => ({count: state.count + 1}))
}
return (<div><button onClick={handleClick}>+1</button><p>{this.state.count}</p></div>);
}
}

它的执行流程是这样的:

如果调用setState 函数时,事务没有被开启,那么 React 便会开启一个临时的事务,在setState 函数调用完毕后便会结束事务。

我们康康这样的流程是什么样的:

如果中间有一些异步代码,React并不会等异步代码执行完毕再关闭事务。 这意味这,异步代码执行完毕后,事务已经被关闭了,所以后面所有的 setState 的执行都是针对单次 setState 调用的,setState 调用完毕,就会关闭事务。

我们还是以事件中的异步代码为例:

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
class ApiList extends React.Component {
constructor(props) {
super(props);
this.state = { list: [] };
}

render() {
let handleClick = async () => {
let res = await fetch("https://api.github.com");
let data = await res.json();
console.log(this.state.list);
this.setState({
list: Object.entries(data),
});
console.log(this.state.list);
};
return (
<div>
<button onClick={handleClick}>加载数据</button>
<ul>
{this.state.list.map((x, i) => (
<li key={i}>
{x[0]}:{x[1]}
</li>
))}
</ul>
</div>
);
}
}

康康它的执行流程:

对于浏览器的原生事件,它的调用绕过了 React 的事务机制,所以没有走 React 的事务调用流程,它的每次 setState 调用都是在事务处理未开始的情况,那么setState 调用完毕就会刷新界面。

  • 标题: React(4-1):状态
  • 作者: ObjectKaz
  • 创建于: 2021-05-03 06:01:42
  • 更新于: 2023-05-25 17:09:14
  • 链接: https://www.objectkaz.cn/8f6ed96fc9a3.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
此页目录
React(4-1):状态