如何解决setState合并执行问题?

前言

之前写过一篇React总结文章,里面有介绍到this.setState是像Object.assign一样是合并执行的,当时用了一种比较hack的方法,后来觉得很不妥,于是经过谷歌和查看文档,知道了还有这么一个方法,这里分享给大家。

原问题

触发increment后,执行的结果是什么,答案是2。

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 = {
count: 1
};
}
increment() {
let countNum = this.state.count; // 1
this.setState({
count: countNum + 5
});
this.setState({
count: countNum + 1
});
}
render() {
return (
<div>
<p>{this.state.count}</p>
<button onClick={this.increment.bind(this)}>increment</button>
</div>
);
}
}

这里我再提一次为什么会出现这样的情况:

因为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。

当时我给出的方法是用setTimeout方法将两次setState隔离触发,虽然是达到了效果,但是因为setTimeout复杂的内部机制和页面复杂的交互逻辑,很大的肯能会导致bug,这里我向大家介绍一种正规的处理方法。

优雅处理方式

我们之前在setState的时候,是通过向setState传入一个对象来改变state,但是React文档中还指明,可以向里面传入一个函数,我们将上面的累加器采用另外的方式来实现一次,在setState()的时候,我们采用传入一个函数的方式来更新我们的 state:

在此之前,我们分别介绍setState的两个参数:

  • prevState:

这是指每次调用setState之前的state状态,我们可以这么理解(但是原理不是这样的),他和普通this.state的区别在于:prevState是同步更新的,this.state是异步更新的。

  • props:

这里是个很大的坑,我们千万不能理解props是自己随便设置的一个变量,这个props就是我们在这个组件上使用的this.props,是父组件向子组件传递的props.

1
2
3
4
5
6
7
8
9
10
increment() {
let countNum = this.state.count; // 1
this.setState((prevState, props) => ({
count: prevState.count + 5
}));
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
}
// <p>{this.state.count}</p> // 7

所以我们可以总结,与传入对象更新state的方式不同,我们传入函数来更新state的时候,React会把我们更新state的函数加入到一个队列里面,然后,按照函数的顺序依次调用。同时,为每个函数传入state的前一个状态,这样,就能更合理的来更新我们的 state 了。

总结

虽然学React也有好几个月的时间,但是对其中的一些细节还是没有理解到位,时刻提醒自己多看文档多看文档多看文档,千万不能总是顾着业务开发,有时候我们会发现,我们绞尽脑汁解决一个问题后,在某一天你会发现,其实React早就有解决这个问题的方法,而你却南辕北辙,浪费很多时间。

Business may be troublesome, but idleness is pernicious. :)