React 中setState更新state何时同步何时异步?
先说结论
- 由
React
控制的事件处理程序,以及生命周期内调用setState
是异步更新state
React
控制之外的事件中调用setState
是同步更新state
,比如原生js绑定事件、setTimeout
/setInrerval
等。
setState
的“异步”并不是说内部由异步代码实现,本身的执行过程和代码都是同步的。
之所以会有一种异步方法的表现形式,归根结底还是因为React
框架本身的性能机制所导致的。因为每次调用setState
都会触发更新,异步操作是为了提高性能,将多个状态合并一起更新,减少re-render
调用。
我们来看一些例子:
React封装事件
代码语言:javascript复制state = {
number: 1
}
componentDidMount() {
this.setState({ number: 3 });
console.log(this.state.number); // 1
}
由此可见该事件处理程序中的setState
是异步更新state
的。
setTimeout
代码语言:javascript复制state = {
number: 1
}
componentDidMount() {
setTimeout(() => {
this.setState({ number: 3 });
}, 0);
console.log(this.state.number); // 3
}
上面我们讲到,setState
本身并不是一个异步方法,之所以会变现出一种异步的形式,是因为React
框架本身的一种性能优化机制。那么基于这一点,假如我们能绕过React
的机制,就可以令setState
以同步的形式体现。
原生事件
代码语言:javascript复制state = {
number: 1
}
componentDidMount() {
document.body.addEventListener('click', this.resetState, false);
}
resetState() {
this.setState({ number: 3 });
console.log(this.state.number); // 3
}
同样的,原生事件也可以绕过React
的性能优化机制,达到同步更新的表现。
React是如何控制异步和同步的?
在React
的setState
函数实现中,会根据一个变量isBatchingUpdates
判断是否直接更新this.state
,还是放入队列中延时更新。
而isBatchingUpdates
默认是false
,标识setState
是同步更新this.state
。但是有一个函数batchedUpdates
会把isBatchingUpdates
修改为true
,而当React
在调用事件处理函数之前就会先调用这个函数将isBatchingUpdates
修改为true
。这样由React
控制的事件处理过程setState
就不会同步更新this.state
。
function enqueueUpdate(component){
//injected注入的
ensureInjected();
//如果不处于批量更新模式
if(!batchingStrategy.isBatchingUpdates){
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
//如果处于批量更新模式
dirtyComponents.push(component);
}
事实上setState
内部的执行过程是很复杂的,大致过程包括更新state
,创建新的VNode
,再经过diff
算法对比差异,决定渲染哪一部分以及怎么渲染,最终形成最新的UI。这一过程包含组件的四个生命周期函数:
shouComponentUpdate
componentWillUpdate
render
componentDidUpdate
如果是子组件并且依赖父组件,还会执行一个钩子函数componentWillReceiveProps
。
假如setState
是同步更新的,每次更新这个过程都要完整执行一次,无疑会造成性能问题。事实上这些生命周期为纯函数,对性能还好,但是diff比较、更新DOM总消耗时间和性能吧。
在“异步”中如果对同一个值进行多次setState
, setState
的批量更新策略会对其进行覆盖,取最后一次的执行。
state = {
number: 1
}
handleClick() {
const number = this.state.number;
this.setState({ number: number 1 });
this.setState({ number: number 1 });
this.setState({ number: number 1 });
}
最终的结果只加了1。
如果是同时 setState
多个不同的值,在更新时会对其进行合并批量更新。
hanldeClick() {
this.setState({ name: 'Clearlove' });
this.setState({ age: 18 });
}
在hanldeClick
处理程序中调用了两次setState
,但是render
只执行了一次。因为React
会将多个this.setState
产生的修改放在一个队列里进行批延时处理。
如何获取“异步”更新后的数据?
setState
提供了一个回调函数供开发者使用,在回调函数中,我们可以实时的获取到更新之后的数据。还是以刚才的例子做示范:
state = {
number: 1
}
componentDidMount() {
this.setState({ number: 3 }, () => {
console.log(this.state.number); // 3
});
}