面试官:react中的setState是同步的还是异步的
hello,这里是潇晨,大家在面试的过程是不是常常会遇到这样的问题,react的setState是同步的还是异步的,这个问题答复的时候肯定要残缺,来看上面这几个例子:
例子1:点击button触发更新,在handle函数中会调用两次setState
export default class App extends React.Component { state = { num: 0, }; updateNum = () => { console.log("before", this.state.num); this.setState({ num: this.state.num + 1 }); this.setState({ num: this.state.num + 1 }); console.log("after", this.state.num); }; render() { const { num } = this.state; console.log("render", num); return <button onClick={this.updateNum}>hello {num}</button>; }}//打印后果//render 0//before 0//after 0//render 1
例子2:例子1的两次setState在setTimeout回调中执行
export default class App extends React.Component { state = { num: 0, }; updateNum = () => { console.log("before", this.state.num); setTimeout(() => { this.setState({ num: this.state.num + 1 }); this.setState({ num: this.state.num + 1 }); console.log("after", this.state.num); }, 0); }; render() { const { num } = this.state; console.log("render", num); return <button onClick={this.updateNum}>hello {num}</button>; }}//打印后果//render 0//before 0//render 1//render 2//after 2
例子3:用unstable_batchedUpdates在setTimout中执行,unstable_batchedUpdates的回调函数中调用两次setState
import { unstable_batchedUpdates } from "react-dom";export default class App extends React.Component { state = { num: 0, }; updateNum = () => { console.log("before", this.state.num); setTimeout(() => { unstable_batchedUpdates(()=>{ this.setState({ num: this.state.num + 1 }); this.setState({ num: this.state.num + 1 }); console.log("after", this.state.num); }) }, 0); }; render() { const { num } = this.state; console.log("render", num); return <button onClick={this.updateNum}>hello {num}</button>; }}//打印后果//render 0//before 0//after 0//render 1
例子4:两次setState在setTimeout回调中执行,然而用concurrent模式启动,也就是调用ReactDOM.unstable_createRoot启动利用。
import React from "react";import ReactDOM from "react-dom";export default class App extends React.Component { state = { num: 0, }; updateNum = () => { console.log("before", this.state.num); setTimeout(() => { this.setState({ num: this.state.num + 1 }); this.setState({ num: this.state.num + 1 }); console.log("after", this.state.num); }, 0); }; render() { const { num } = this.state; console.log("render", num); return <button onClick={this.updateNum}>hello {num}</button>; }}ReactDOM.unstable_createRoot(rootEl).render(<App />); //打印后果//render 0//before 0//after 0//render 1
batchedUpdates
简略来说,在一个上下文中同时触发屡次更新,这些更新会合并成一次更新,例如
onClick() { this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 });}
在之前的react版本中如果脱离以后的上下文就不会被合并,例如把屡次更新放在setTimeout中,起因是处于同一个context的屡次setState的executionContext都会蕴含BatchedContext,蕴含BatchedContext的setState会合并,当executionContext等于NoContext,就会同步执行SyncCallbackQueue中的工作,所以setTimeout中的屡次setState不会合并,而且会同步执行。
onClick() { setTimeout(() => { this.setState({ count: this.state.count + 1 }); this.setState({ count: this.state.count + 1 }); });}
export function batchedUpdates<A, R>(fn: A => R, a: A): R { const prevExecutionContext = executionContext; executionContext |= BatchedContext; try { return fn(a); } finally { executionContext = prevExecutionContext; if (executionContext === NoContext) { resetRenderTimer(); //executionContext为NoContext就同步执行SyncCallbackQueue中的工作 flushSyncCallbackQueue(); } }}
在Concurrent mode下,下面的例子也会合并为一次更新,根本原因在如下一段简化的源码,如果屡次setState,会比拟这几次setState回调的优先级,如果优先级统一,则先return掉,不会进行前面的render阶段
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) { const existingCallbackNode = root.callbackNode;//之前曾经调用过的setState的回调 //... if (existingCallbackNode !== null) { const existingCallbackPriority = root.callbackPriority; //新的setState的回调和之前setState的回调优先级相等 则进入batchedUpdate的逻辑 if (existingCallbackPriority === newCallbackPriority) { return; } cancelCallback(existingCallbackNode); } //调度render阶段的终点 newCallbackNode = scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root), ); //...}
那为什么在Concurrent mode下,在setTimeout回调屡次setState优先级统一呢,因为在获取Lane的函数requestUpdateLane,只有第一次setState满足currentEventWipLanes === NoLanes,所以他们的currentEventWipLanes参数雷同,而在findUpdateLane中schedulerLanePriority参数也雷同(调度的优先级雷同),所以返回的lane雷同。
export function requestUpdateLane(fiber: Fiber): Lane { //... if (currentEventWipLanes === NoLanes) {//第一次setState满足currentEventWipLanes === NoLanes currentEventWipLanes = workInProgressRootIncludedLanes; } //... //在setTimeout中schedulerLanePriority, currentEventWipLanes都雷同,所以返回的lane也雷同 lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes); //... return lane;}
总结:
legacy模式下:命中batchedUpdates时是异步 未命中batchedUpdates时是同步的
concurrent模式下:都是异步的
视频解说(高效学习):点击学习
往期react源码解析文章:
1.开篇介绍和面试题
2.react的设计理念
3.react源码架构
4.源码目录构造和调试
5.jsx&外围api
6.legacy和concurrent模式入口函数
7.Fiber架构
8.render阶段
9.diff算法
10.commit阶段
11.生命周期
12.状态更新流程
13.hooks源码
14.手写hooks
15.scheduler&Lane
16.concurrent模式
17.context
18事件零碎
19.手写迷你版react
20.总结&第一章的面试题解答