一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情。
核心描述
-
说明:所谓同步还是异步,指的是调用 setState 方法之后是否马上能得到新的 state 值。
-
表现:
- 常规模式
legacy
模式下:- index.js 核心代码
// 默认渲染为 legacy 模式 ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );
- 示例核心代码:
// 创建一个类组件,在 componentDidMount 钩子中编写 demo componentDidMount () { // 表现为同步输出 setTimeout(()=>{ this.setState({number:3}) console.log(this.state.number) // 输出 3 this.setState({number:4}) console.log(this.state.number) // 输出 4 },0) // 表现为异步输出 this.setState({ a:1 }) console.log(this.state.a) // 输出 undefined }
- concurrent 模式下:
- index.js 核心代码:
import ReactDOM from 'react-dom'; // 启动 concurrent 渲染模式 ReactDOM.unstable_createRoot(document.getElementById('root')).render(<React.StrictMode><App /></React.StrictMode>);
- 示例核心代码:
// 表现为异步输出 setTimeout(()=>{ this.setState({number:3}) console.log(this.state.number) // 输出 undefined this.setState({number:4}) console.log(this.state.number) // 输出 undefined },0) // 表现为异步输出 this.setState({ a:1 }) console.log(this.state.a) // 输出 undefined
- 常规模式
-
结论:在不同的情况下,会表现出不同的结果。如果 this.setState 方法调用进入的 React 内部的任务调度,则会表现为异步;如果未进入 React 的任务调度,则会表现为同步。这是 React 内部合并 setState 执行的一种性能优化的实现。
- 同步的表现情况:
- 首先在 legacy 模式下
- 在执行上下文为空的时候去调用 setState
- 可以使用异步调用如setTimeout, Promise, MessageChannel等
- 可以监听原生事件, 注意不是合成事件, 在原生事件的回调函数中执行 setState 就是同步的
- 异步的表现情况:
- 如果是合成事件中的回调, executionContext |= DiscreteEventContext, 所以不会进入, 最终表现出异步
- concurrent 模式下都会为异步
- 同步的表现情况:
知识拓展
源码参考 React 版本为 v17.0.2
- 在 React 中调用 setState 方法时,影响其同步/异步表现调度核心方法为 scheduleUpdateOnFiber
- setState 核心调用链:
this.setState
->this.updater.enqueueSetState
->scheduleUpdateOnFiber
- 源码分析:
checkForNestedUpdates();
warnAboutRenderPhaseUpdatesInDEV(fiber);
var root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
return null;
} // Mark that the root has a pending update.
markRootUpdated(root, lane, eventTime);
if (root === workInProgressRoot) {
// Received an update to a tree that's in the middle of rendering. Mark
// that there was an interleaved update work on this root. Unless the
// `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
// phase update. In that case, we don't treat render phase updates as if
// they were interleaved, for backwards compat reasons.
{
workInProgressRootUpdatedLanes = mergeLanes(workInProgressRootUpdatedLanes, lane);
}
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
// The root already suspended with a delay, which means this render
// definitely won't finish. Since we have a new update, let's mark it as
// suspended now, right before marking the incoming update. This has the
// effect of interrupting the current render and switching to the update.
// TODO: Make sure this doesn't override pings that happen while we've
// already started rendering.
markRootSuspended$1(root, workInProgressRootRenderLanes);
}
} // TODO: requestUpdateLanePriority also reads the priority. Pass the
// priority as an argument to that function and this one.
var priorityLevel = getCurrentPriorityLevel();
if (lane === SyncLane) {
if ( // Check if we're inside unbatchedUpdates
(executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
(executionContext & (RenderContext | CommitContext)) === NoContext) {
// Register pending interactions on the root to avoid losing traced interaction data.
schedulePendingInteractions(root, lane); // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
// root inside of batchedUpdates should be synchronous, but layout updates
// should be deferred until the end of the batch.
console.log('=====> debuger: 01',lane, SyncLane)
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
console.log('======> debugger:02', executionContext, NoContext) // 在 legacy 模式下,会打印此 log,在 setTimeout 方法调用时, executionContext 与 NoContext 相等
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
resetRenderTimer();
flushSyncCallbackQueue();
}
}
} else {
// Schedule a discrete update but only if it's not Sync.
if ((executionContext & DiscreteEventContext) !== NoContext && ( // Only updates at user-blocking priority or greater are considered
// discrete, even inside a discrete event.
priorityLevel === UserBlockingPriority$2 || priorityLevel === ImmediatePriority$1)) {
// This is the result of a discrete event. Track the lowest priority
// discrete update per root so we can flush them early, if needed.
if (rootsWithPendingDiscreteUpdates === null) {
rootsWithPendingDiscreteUpdates = new Set([root]);
} else {
rootsWithPendingDiscreteUpdates.add(root);
}
} // Schedule other updates after in case the callback is sync.
console.log('======> debugger:03') // 当使用 concurrent 模式时,会输出此 log
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
}
参考资料
- 【React深入】setState的执行机制:juejin.cn/post/684490…
- 【官方文档】State 的更新可能是异步的:zh-hans.reactjs.org/docs/state-…
浏览知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。