更合理的 setState()
原文发表在我的博客:http://www.erichain.me/2017/04/17/2017-04-17-more-reasonable-setstate/
React 是我做前端以来接触到的第三个框架(前两个分别是 Angular 和 Vue),无论是从开发体验上和效率上,这都是一门非常优秀的框架,非常值得学习。
原谅我说了一些废话,以下是正文。
借助于 Redux,我们可以轻松的对 React 中的状态进行管理和维护,同时,React 也为我们提供了组件内的状态管理的方案,也就是 setState()
。本文不会涉及到 Redux,我们将从 Component 的角度来说明你不知道的以及更合理的 setState()
。
先说说大家都知道的
在 React 文档的 State and Lifecycle 一章中,其实有明确的说明 setState()
的用法,向 setState()
中传入一个对象来对已有的 state 进行更新。
比如现在有下面的这样一段代码:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
count: this.state.count + 1
};
}
}
我们如果想要对这个 state 进行更新的话,就可以这样使用 setState()
:
this.setState({
count: 1
});
你可能不知道的
最基本的用法世人皆知,但是,在 React 的文档下面,还写着,处理关于异步更新 state 的问题的时候,就不能简单地传入对象来进行更新了。这个时候,需要采用另外一种方式来对 state 进行更新。
setState()
不仅能够接受一个对象作为参数,还能够接受一个函数作为参数。函数的参数即为 state 的前一个状态以及 props。
所以,我们可以向下面这样来更新 state:
this.setState((prevState, props) => ({ count: prevState.count + 1 }));
这样写的话,能够达到同样的效果。那么,他们之间有什么区别呢?
区别
我们来详细探讨一下为什么会有两种设置 state 的方案,他们之间有什么区别,我们应该在何时使用何种方案来更新我们的 state 才是最好的。
此处,为了能够明确的看出 state 的更新,我们采用一个比较简单的例子来进行说明。
我们设置一个累加器,在 state 上设置一个 count
属性,同时,为其增加一个 increment
方法,通过这个 increment
方法来更新 count
。
此处,我们采用给 setState()
传入对象的方式来更新 state,同时,我们在此处设置每调用一次 increment
方法的时候,就调用两次 setState()
。具体的原因我们在后文中会讲解。
具体的代码如下:
class IncrementByObject extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
this.increment = this.increment.bind(this);
}
// 此处设置调用两次 setState()
increment() {
this.setState({
count: this.state.count + 1
});
this.setState({
count: this.state.count + 1
});
}
render() {
return (
<div>
<button onClick={this.increment}>IncrementByObject</button>
<span>{this.state.count}</span>
</div>
);
}
}
ReactDOM.render(
<IncrementByObject />,
document.getElementById('root')
);
这时候,我们点击 button 的时候,count
就会更新了。但是,可能与我们所预期的有所差别。我们设置了点击一次就调用两次 setState()
,但是,count
每一次却还是只增加了 1,所以这是为什么呢?
其实,在 React 内部,对于这种情况,采用的是对象合并的操作,就和我们所熟知的 Object.assign()
执行的结果一样。
比如,我们有以下的代码:
Object.assign({}, { a: 2, b: 3 }, { a: 1, c: 4 });
那么,我们最终得到的结果将会是 { a: 1, b: 3, c: 4 }
。对象合并的操作,属性值将会以最后设置的属性的值为准,如果发现之前存在相同的属性,那么,这个属性将会被后设置的属性所替换。所以,也就不难理解为什么我们调用了两次 setState()
之后,count
依然只增加了 1 了。
用简短的代码说明就是这样:
this.setState({
count: this.state.count + 1
});
// 同理于
Object.assign({}, this.state, { count: this.state.count + 1 });
以上是我们采用对象的方式传入 setState()
来更新 state 的说明。接下来我们再看看使用函数的方式来更新 state 会有怎么样的效果呢?
我们将上面的累加器采用另外的方式来实现一次,在 setState()
的时候,我们采用传入一个函数的方式来更新我们的 state。
class IncrementByFunction extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
this.increment = this.increment.bind(this);
}
increment() {
// 采用传入函数的方式来更新 state
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
this.setState((prevState, props) => ({
count: prevState.count + 1
}));
}
render() {
return (
<div>
<button onClick={this.increment}>IncrementByFunction</button>
<span>{this.state.count}</span>
</div>
);
}
}
ReactDOM.render(
<IncrementByFunction />,
document.getElementById('root')
);
当我们再次点击按钮的时候,就会发现,我们的累加器就会每次增加 2 了。
我们可以通过查看 React 的源代码来找出这两种更新 state 的区别 (此处只展示通过传入函数进行更新的方式的部分源码)。
在 React 的源代码中,我们可以看到这样一句代码:
this.updater.enqueueSetState(this, partialState, callback, 'setState');
然后,enqueueSetState
函数中又会有这样的实现:
queue.push(partialState);
enqueueUpdate(internalInstance);
所以,与传入对象更新 state 的方式不同,我们传入函数来更新 state 的时候,React 会把我们更新 state 的函数加入到一个队列里面,然后,按照函数的顺序依次调用。同时,为每个函数传入 state 的前一个状态,这样,就能更合理的来更新我们的 state 了。
问题所在
那么,这就是传入对象来更新 state 会导致的问题吗?当然,这只是问题之一,还不是主要的问题。
我们之前也说过,我们在处理异步更新的时候,需要用到传入函数的方式来更新我们的 state。这样,在更新下一个 state 的时候,我们能够正确的获取到之前的 state,并在在其基础之上进行相应的修改。而不是简单地执行所谓的对象合并。
所以说,我们建议,在使用 setState
的时候,采用传入函数来更新 state 的方式,这样也是一个更合理的方式。
我在 CodePen 上将这两个效果组合到了一起,感兴趣的话,你可以去试着点击一下。
See the Pen React Functional setState by Erichain (@Erichain) on CodePen.