setState函数的异步性
简述
在某些情况下,React框架出于性能优化考虑,可能会将多次state更新合并成一次更新。正因为如此,setState实际上是一个异步的函数。
但是,有一些行为也会阻止React框架本身对于多次state更新的合并,从而让state的更新变得同步化。
比如: eventListeners, Ajax, setTimeout 等等。
详解
当setState() 函数执行的时候,函数会创建一个暂态的state作为过渡state,而不是立即修改this.state。
如果在调用setState()函数之后尝试去访问this.state,你得到的可能还是setState()函数执行之前的结果。
在使用setState()的情况下,看起来同步执行的代码其实执行顺序是得不到保证的。原因上面也提到过,React可能会将多次state更新合并成一次更新来优化性能。
运行下面这段代码,你会发现当和addEventListener, setTimeout 函数或者发出AJAX call的时候,调用setState, state会发生改变。并且render函数会在setState()函数被触发之后马上被调用。那么到底发生了什么呢?事实上,类似setTimeout()函数或者发出ajax call的fetch函数属于调用浏览器层面的API,这些函数的执行并不存在与React的上下文中,所以React并不能够像控制其他存在与其上下文中的函数一样,将多次state更新合并成一次。
在上面这些例子中,React框架之所以在选择在调用setState函数之后立即更新state而不是采用框架默认的方式,即合并多次state更新为一次更新,是因为这些函数调用(fetch,setTimeout等浏览器层面的API调用)并不处于React框架的上下文中,React没有办法对其进行控制。React在此时采用的策略就是及时更新,确保在这些函数执行之后的其他代码能拿到正确的数据(即更新过的state)。
class TestComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
dollars: 10
}
this.onMouseLeaveHandler = this.onMouseLeaveHandler.bind(this);
this.onTimeoutHandler = this.onTimeoutHandler.bind(this);
this.onAjaxCallback = this.onAjaxCallback.bind(this);
this.onClickHandler = this.onClickHandler.bind(this);
}
componentDidMount() {
// Add custom event via `addEventListener`
//
// The list of supported React events does include `mouseleave`
// via `onMouseLeave` prop
//
// However, we are not adding the event the `React way` - this will have
// effects on how state mutates
//
// Check the list here - https://facebook.github.io/react/docs/events.html
document.getElementById('testButton').addEventListener('mouseleave', this.onMouseLeaveHandler);
// Add JS timeout
//
// Again,outside React `world` - this will also have effects on how state
// mutates
setTimeout(this.onTimeoutHandler, 10000);
// Make AJAX request
fetch('https://api.github.com/users')
.then(this.onAjaxCallback);
}
onClickHandler = () => {
console.log('State before (_onClickHandler): ' + JSON.stringify(this.state));
this.setState({
dollars: this.state.dollars + 10
});
console.log('State after (_onClickHandler): ' + JSON.stringify(this.state));
}
onMouseLeaveHandler = () => {
console.log('State before (mouseleave): ' + JSON.stringify(this.state));
this.setState({
dollars: this.state.dollars + 20
});
console.log('State after (mouseleave): ' + JSON.stringify(this.state));
}
onTimeoutHandler = () => {
console.log('State before (timeout): ' + JSON.stringify(this.state));
this.setState({
dollars: this.state.dollars + 30
});
console.log('State after (timeout): ' + JSON.stringify(this.state));
}
onAjaxCallback = (err, res) => {
if (err) {
console.log('Error in AJAX call: ' + JSON.stringify(err));
return;
}
console.log('State before (AJAX call): ' + JSON.stringify(this.state));
this.setState({
dollars: this.state.dollars + 40
});
console.log('State after (AJAX call): ' + JSON.stringify(this.state));
}
render() {
console.log('State in render: ' + JSON.stringify(this.state));
return (
<button
id="testButton"
onClick={this.onClickHandler}>
'Click me'
</button>
);
}
}
ReactDOM.render(
<TestComponent />,
document.getElementById('app')
);
解决setState函数异步的办法?
根据React官方文档,setState函数实际上接收两个参数,其中第二个参数类型是一个函数,作为setState函数执行后的回调。通过传入回调函数的方式,React可以保证传入的回调函数一定是在setState成功更新this.state之后再执行。
例子
_onClickHandler: function _onClickHandler() {
console.log('State before (_onClickHandler): ' + JSON.stringify(this.state));
this.setState({
dollars: this.state.dollars + 10
}, () => {
console.log('Here state will always be updated to latest version!');
console.log('State after (_onClickHandler): ' + JSON.stringify(this.state));
});
}
更多关于setState的小知识
其实setState作为一个函数,本身是同步的。只是因为在setState的内部实现中,使用了React updater的enqueueState 或者 enqueueCallback方法,才造成了异步。
下面这段是React源码中setState的实现:
ReactComponent.prototype.setState = function(partialState, callback) {
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null,
'setState(...): takes an object of state variables to update or a ' +
'function which returns an object of state variables.'
);
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
而updater的这两个方法,又和React底层的Virtual Dom(虚拟DOM树)的diff算法有紧密的关系,所以真正决定同步还是异步的其实是Virtual DOM的diff算法。