背景
关于React 中的setState,我们常常会看见一个问题,“React中的setState是同步还是异步的?”我们经常会这样答:可异步,可同步;这篇文章就来解释一下其中的原因
现象
我们先看看,在什么样的场景中是异步的:
class App extends React.Component{
constructor(props){
super(props);
this.btnClick = this.btnCLick.bind(this);
}
state = {
a: 1
}
btnClick(){
this.setState({
a: this.state.a + 1
});
this.setState({
a: this.state.a + 1
});
console.log('点击时的a的值为:',this.state.a);
}
render(){
return (
<>
<div>hello william</div>
<button onClick={this.btnClick}>点击按钮</button>
</>
)
}
}
现象:点击按钮时的a的值为:1,在代码里我们setState了两次,值只发生了一次变化,这样看确实是异步的
我们把代码稍微改动一下:
class App extends React.Component{
constructor(props){
super(props);
this.btnClick = this.btnCLick.bind(this);
}
state = {
a: 1
}
btnClick(){
setTimeout(() => {
this.setState({
a: this.state.a + 1
})
console.log('点击时的a的值',this.state.a);
},0)
}
render(){
return (
<>
<div>hello william</div>
<button onClick={this.btnClick}>点击按钮</button>
</>
)
}
}
现象:点击时的a的值为:2,这是的值通过setState被修改了两次,这样看就是同步的
看下源码
为什么会这样?我们看一下源码:
1、从方法的挂载开始
Component.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, callback, 'setState');
};
2、enqueueSetState方法
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
const update = createUpdate(expirationTime);
update.payload = payload;
if (callback !== undefined && callback !== null) {
if (__DEV__) {
warnOnInvalidCallback(callback, 'setState');
}
update.callback = callback;
}
flushPassiveEffects();
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
},
当我们点击button按钮触发onClick事件的时候,会通过合成事件分发对应的回调函数,执行onClick中的内容。在onClick函数中,我们进行了一次setState。执行了enqueueSetState函数。
函数内有几个重要的步骤:
- createUpdate:创建了一个update对象。
- enqueueUpdate:创建updateQueue对象,将update对象存入到当前Fiber节点的updateQueue对象中的firstUpdate和lastUpdate中。
- scheduleWork:调用requestWork函数。
3、requestWork函数
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
addRootToSchedule(root, expirationTime);
if (isRendering) {
return;
}
if (isBatchingUpdates) {
if (isUnbatchingUpdates) {
// flush it now.
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, false);
}
return;
}
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}
如果这次的setState并不是由合成事件触发的,那么isBatchingUpdates将会为false。如果为false就会直接执行performSyncWork函数了,马上对这次setState进行diff和渲染。
连续setState多次只触发一次render就是因为经过了合成事件的关系,合成事件先执行了onClick函数中的setState,修改了Fiber的updateQueue对象的任务,执行完onClick函数体后,再由合成事件让根Fiber进行渲染(。所以无论你在一个事件内触发无数次setState,也只会触发一次render。
生命周期中:在生命周期内进行setState的话当React的组件还没有渲染完成的时候,isRendering是为true。
结论
- 异步:React 会先找到我们注册的 vnode 和 vnode 内的对应事件,从而在执行前,先把 isBatchingUpdate
这个变量打开。只要我们的方法没完成,由于变量锁的存在,就会一直让我们的修改只停留在更新中状态内,一直不会更新到实际的 state
上。直到我们的方法执行完,事务的后置函数就会关闭我们的 isBatchingUpdate,并执行渲染操作,至此整个批量更新就完成了。 - 同步:setTimeout 里面会同步是由于 setTimeout会把里面的函数放到下一个宏任务内,这样就刚好跳出了事务的控制,就会显示出同步更新的情况。这里就是Javascript 的 Event-loop 机制;另外,在原生事件中,绕过了React,不会触发isBatchingUpdates变量的改变,所以也会同步进行更新渲染