还有这操作???

前言

学习React也有那么久了,遇到一些很奇怪的错误,和一些很奇怪的坑,特别是state和setState中的坑,让我久久不能忘记,于是写下了这篇,记录自己的错误与自己填的坑,供大家参考,如有错误,欢迎指正~

管理好state并不是那么简单

这是初学者开发React项目会遇到最大的坑之一,因为要良好地使用state需要转变很多思想,其实主要也是要深度理解React中state较为复杂的机制,下面是我遇到的这些坑:

  1. 不能直接更新状态,如:this.state.color=”red”;
  2. setState是异步的,不能在setState后直接使用新的state
  3. 不能在setState里面调用this.state
  4. setState是合并调用的,并不是单个setState单次调用

对于以上四点,我们逐个来分析,

1.正确改变state的姿势

刚刚学习React的时候,受于惯性思维,比如想要改变state的值,直接像下面一样改变:

1
this.state.comment = 'Hello'; //wrong

这会导致什么情况呢?我们用一个例子来展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class App extends React.Component {
constructor(props){
super(props)
this.state = {
color:'red'
}
}
changeColor(){
this.state.color = 'green'
setTimeout(() => {
console.log(this.state.color)
},100)
}
render() {
return (
<div>
<p>{this.state.color}</p>
<button onClick={this.changeColor.bind(this)} >change</button>
</div>
);
}
}

一般我们觉得,按钮点击后,this.state.color就成了”green”,定时器里面输出green,然后p标签的内容也变成green,不用你亲自试,我做了个gif图表示:

image

我们惊讶的发现p标签中的内容并没有变化,但是this.state.color确实改变了呀。我们不难分析出,如果使用this.state.color=“green”来改变state的话,state确实会改变,但是并没有驱动组件进行更新渲染,而使用setState就正常渲染,这说明this.setState()做了两件事:1. 改变state值 2. 驱动组件重新渲染。所以再次强调切勿用JavaScript改变对象的值一样来改变React的state。

2. this.setState(),更新是异步的

setState是异步的,不能在setState后直接使用新的state,比如像下面一样:

1
2
3
4
5
6
7
setColor(){
let {color} = this.state
this.setState({
color: 'green' //新值
})
console.log(this.state.color)//旧值
}

这也是我们常常遇到的问题,但是很多时候,有必须要在setState后面直接使用最新的this.state怎么办?我总结了三种办法:

  1. 设置一个定时器
1
2
3
4
5
6
7
8
setColor(){
this.setState({
color:'green'
})
setTimeout(()=>{
console.log(this.state.color) // green
},0)
}

这都不难理解,在eventloop中打印this.state.color的次序排在this.setState后面,因此能获得最新值

  1. 使用this.setState()的回调函数
1
2
3
4
5
6
7
setColor(){
this.setState({
color:'green'
},()=>{
console.log(this.state.color)
})
}

这是React为了方便很多喜欢用回调函数的小伙伴们创造的福利~

3. 不能在setState里面调用this.state

相信很多小伙伴会写出这样的代码,其实这是很容易导致bug的,因为从第二点我们可以看书,this.state可能是异步更新的,所以不能依靠他来计算下一个值。

1
2
3
this.setState({
counter: this.state.counter + this.props.increment,
});

4. setState是合并调用的,并不是单个setState单次调用

这个问题确实让我想了很久,但是又不知道为什么,当初遇到的问题是下面代码,后来看了github一个小伙伴的讲解,说是setState是合并调用,并不是分开来调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
constructor(props){
super(props)
this.state = {
count: 1
}
}
increment(){
let countNum = this.state.count // 1
this.setState({
count: countNum+5
})
this.setState({
count: countNum+1
})
}

问题: 触发一次increment后,count的值是多少

答案:2

别不信,下面是我运行的gif图:
image

这是什么导致的呢?React官方的解释是为了性能优化,一次性将“本轮的setState”合并一起调用,什么意思呢?不知道你用过Object.assign()没有,如下:

1
Object.assign({}, { a: 2, b: 3 }, { a: 1, c: 4 }); //{a:1,c:4,b:3}

没错,setState也是这么做的,如下:

1
Object.assign({count: countNum+5},{count: countNum+1})

这么一说,就好理解了,React官方这么做也不是没有道理的,如果你注重代码质量,就不会在同一时段重复在setState中改变同一个state。

那么问题来了,我偏要这样怎么办?那我们就得“人为”地将这两次setState分开来,定时器就是一个比较hack的方案:

1
2
3
4
5
6
7
8
9
10
11
12
increment(){
let countNum = this.state.count
this.setState({
count: countNum+1
})
setTimeout(()=>{
let countNum2 = this.state.count
this.setState({
count: countNum2+1
})
})
}

总结

其实我一直认为自己遇到的坑都不能叫坑,只是自己没有认真阅读文档而已,也反应出自己浮躁的心情,急于求成,这是件非常可怕的事情,听过这么一句话:“好的代码,都是用时间熬出来的”,所以在这次总结后,我将会就React文档再次认真阅读。

Truth never fears investigation :)