内容更新于: 2022-05-30 11:10:48
多次setState
前言:从几个问题开始:在react中setState是同步还是异步?多次setState之后render执行的次数和什么有关?通过阅读本文,你可以得到明确的答案,和部分的源码。
1.setState是同步还是异步?
生命周期(componentDidMount)中是异步
合成事件中是异步
setTimeout中是同步
原生事件中是同步
1-1.生命周期(componentDidMount)中是异步
如果生命周期这里的异步并不是实际意义上的异步。只是代码执行被开关限制后的一个状态延后更新。
具体在下一个问题中进行阐述。
1-2.合成事件中是异步
结果同生命周期componentDidMount,具体实现在下一个问题中阐述。
1-3.setTimeout中是同步
因为代码是同步的,并没有开关或者异步回调等方式处理它。
1-4.原生事件中是同步
同setTimeout
2.多次setState之后render执行的次数
这里探讨的问题默认expirationTime优先级是一样的。
假如改变expirationTime(优先级)其执行方式不一样。
// ReactFiberClassComponent
let i = 1
const classComponentUpdater = {
isMounted,
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst);
const currentTime = requestCurrentTime();
const expirationTime = computeExpirationForFiber(currentTime, fiber);
// 为了测试而改变update的过期时间
let updateExpirationTime = expirationTime + (--i)
const update = createUpdate(updateExpirationTime);
}
}
当优先级不一样的时候,会先更新优先级更高的update,同时执行scheduleCallbackWithExpirationTime方法注入回调,ScheduleWork会在适当的时候执行,
从而回过头执行performWork方法进行下一个状态的更新。
2-1.在生命周期中
// ...
componentDidMount () {
this.setState({
age: 1
})
this.setState({
age: 2
})
this.setState({
age: 3
})
}
// ...
答案是一次,确切的说是优先级一样所以render一次。
首先componentDidMount生命周期是在commit阶段触发的。
此时commit并未结束, 变量isCommitting为true、isRendering也为true.
setState是去执行函数scheduleWork
const classComponentUpdater = {
enqueueSetState () {
// ...
flushPassiveEffects();
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
}
}
scheduleWork --> requestWork 因为此时isRendering为true所以并未执行之后的步骤。
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
addRootToSchedule(root, expirationTime);
// console.log('isRendering: ', isRendering)
// last
if (isRendering) {
// Prevent reentrancy. Remaining work will be scheduled at the end of
// the currently rendering batch.
return;
}
// ...
}
所以之后的render函数自然没有执行。
那之后的步骤是如何继续的呢
在开始执行performWork的循环中 在执行完performWorkOnRoot之后回去检查是否还有有过期时间的root
function performWork () {
// ...
while (
nextFlushedRoot !== null &&
nextFlushedExpirationTime !== NoWork &&
minExpirationTime <= nextFlushedExpirationTime
) {
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
// root.expirationTime
findHighestPriorityRoot();
}
// ...
而之前说到的componentDidMount是在commit阶段被执行的。同时setState触发scheduleWork中
会执行markPendingPriorityLevel方法将root的expirationTime重新标记。上述函数中的while
循环会继续执行,更新之后的状态,所以说render只会执行一次。
2-2.在setTimeout中
与setState执行的次数一致
class Main extends React.Component {
componentDidMount () {
setTimeout(() => {
this.setState({
age: 1
})
Logger.info('age: ', this.state.age)
// 1
this.setState({
age: 2
})
Logger.info('age: ', this.state.age)
// 2
this.setState({
age: 3
})
Logger.info('age: ', this.state.age)
// 3
})
}
render () {
const { age, gender } = this.state
Logger.info('render age: ', this.state.age)
// 0
// 1
// 2
// 3
return React.createElement('div', {
onClick: this.onClick.bind(this)
}, `age: ${age} -- gender: ${gender}`)
}
}
// 这里setState引起的render 会执行3次
2-3.在合成事件中
执行一次
首先在requestWork函数中也是开关, 阻止之后的更新。
// requestWork 函数
if (isBatchingUpdates) {
// Flush work at the end of the batch.
// isUnbatchingUpdates 在当前这个问题中 false
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates,
// in which case we should
// flush it now.
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, false);
}
return;
}
在函数interactiveUpdates的finally中执行performSyncWork
function dispatchInteractiveEvent(topLevelType, nativeEvent) {
// Logger.info('dispatchInteractiveEvent', topLevelType)
interactiveUpdates(dispatchEvent, topLevelType, nativeEvent);
}
// 函数interactiveUpdates
try {
return runWithPriority(UserBlockingPriority, () => {
return fn(a, b);
});
} finally {
isBatchingUpdates = previousIsBatchingUpdates;
if (!isBatchingUpdates && !isRendering) {
MainLogger.step(
'ReactFiberScheduler interactiveUpdates',
'finally 执行 performSyncWork()'
)
performSyncWork();
}
}
performSyncWork --> performWork
总结
react的这种机制可以有效的防止多个setState产生的不必要的开支。
本文结束
