当前位置: 首页 > 科技观察

React的SetState是同步的还是异步的?_0

时间:2023-03-13 16:48:15 科技观察

setState是同步的还是异步的?它必须是异步的。你确定吗?然后看看这段代码将打印什么:import{Component}from'react';classDongextendsComponent{constructor(){super();this.state={count:0}}componentDidMount(){setTimeout(()=>{this.setState({count:1});console.log(this.state.count);this.setState({count:2});console.log(this.state.count);});}render(){console.log('render:',this.state.count);返回

{this.state.count}
;}}在setTimeout中两次修改state,打印state的值。如果是异步的,应该打印的时候count没有修改,还是0,所以打印了两次0。然后初始化渲染一次,setState之后再渲染一次,应该是渲染了两次,计数分别为0和2。按照异步的方式分析,确实应该是这样的。执行一下:我们会发现两次打印分别是1和2,也就是说setState是同步修改state,然后每次都触发渲染,所以总共有3次render,分别是0、1、和2分别。那么setState是同步的吗?你确定吗?然后看看这段代码会打印什么?classDongextendsComponent{constructor(){super();this.state={count:0}}componentDidMount(){this.setState({count:1});console.log(this.state.count);这个.setState({count:2});console.log(this.state.count);this.setState({count:3});console.log(this.state.count);}渲染(){控制台。日志('呈现:',this.state.count);返回
{this.state.count}
;}}如果setState是同步的,执行完会修改state,分别打印1,2,3,然后触发三个renders,加上第一个,一共四次,打印0,1,2,3、我们执行一下:三个打印都是0,说明setState是异步的。而三个setState只触发一次render,加上初始render,一共两次,打印0、3。什么鬼,怎么又是异步的?而且不仅class组件的setState是这样的,function组件的useState也是一样的:比如state修改了3次,只会渲染一次:而在setTimeout中,每次state修改了,会渲染:是不是有点晕,setState在什么情况下是同步的,什么情况下是异步的?我们需要从源码中寻找答案,我们来看一下setState的源码。首先我们看一下React的渲染过程:React的渲染过程react使用jsx来描述界面,jsx可以通过babel等编译器编译成render函数,然后执行生成vdom:这个vdom不是直接渲染,但会转换成纤维,稍后再渲染。因为vdom中的每个节点只记录子节点,不记录兄弟节点,所以必须一次不间断地渲染。转换为fiber的链表结构会记录父节点(return)、子节点(child)、兄弟节点(sibling),变得可中断。这里的vdom是一个ReactElement对象:转换成fiber之后就是FiberNode的对象:从vdom转换成fiber的过程叫做reconcile,在转换的过程中会创建对应的dom元素,然后commit到dom在所有转换完成后一次。这个过程不是一次性的,它是通过调度器调度执行的,所以可以分批执行,也就是可以打断。这是React的fiber架构下的渲染过程。理论讲完了,我们来看看对应的源码(这里是v17的源码):react调用schedule和reconcilerender阶段,这个阶段就是将vdom转成fiber。(schedule只是让reconcile可以执行多次,可以被打断,但是做的事情是一样的,所以schedule也是render阶段的一部分)。将fiber更新到dom的过程称为提交阶段。对应源码是这样的:这个performSyncWorkOnRoot是渲染的入口。之前说过,会先执行render阶段,将vdom转化为fbier,然后执行commit更新dom。render阶段会执行一个调度循环:这个循环就是不断的处理每个fiber的reconcile:每个node有beginWork和completeWork两个stage,因为需要将vdom转换成fiber,而vdom是树结构,需要递归处理:不同节点的reconcile逻辑不同:比如会调用function组件,从render中获取vdom,继续reconcile:比如class组件会创建实例,调用render方法,获取vdom,然后继续和好孩子。简而言之,将vdom转换为fiber是一个递归过程。然后进入提交阶段。整个渲染过程的入口是performSyncWorkOnRoot函数。渲染过程结束了,接下来就是setState如何触发渲染过程了:我们知道setState渲染过程的入口是performSyncWorkOnRoot函数。那么,setState修改状态后,触发这个函数就够了吗?确实如此。setState会调用dispathAction,创建一个update对象放到fiber节点的updateQueue上,然后调度渲染:调度update自然就是调度上面提到的performSyncWorkOnRoot函数:react会先从触发了fiber的fiber中找到根fiber节点update,然后调用performSyncWorkOnRoot的函数进行渲染:这是setState之后触发重新渲染的实现。setState是同步的还是异步的在这一段控制。我们看到在判断条件中有一个executionContext,用来标识当前环境是批处理还是非批处理,是执行了render阶段还是commit阶段。其实在执行ReactDOM.render的时候,会先调用unbatchUpdates函数:这个函数会在executionContext:中设置一个unbatachflag,这样更新的时候,会立即执行performSyncWorkOnRoot进行渲染。因为第一次渲染会立即渲染,所以不需要调度。到达commit阶段后,会设置一个commitflag:然后setState就不会再去unbatch分支了。那为什么setTimeout中的setState会同步执行呢?因为setTimeout直接执行的异步代码没有设置executionContext,所以会去到NoContext分支,立即渲染。(这里的flush最终会调用performSyncWorkOnRoot函数来render):有没有办法让setTimeout中执行的函数也有executionContext?其实react17暴露了batchUpdates的api,用它包裹起来,里面的setState会被批量执行:它的源码其实就是设置executionContext:setState全部执行完之后,再flush,调用peformSyncWorkOnRoot渲染,效果是批处理setState也是。其实按理说setState不能异步调用,还是在同一个调用栈中执行,只是顺序不同而已。只能称为批处理或非批处理。这是在react17中处理的。如果是react18,如果使用createRootapi,就不会出现这个问题。甚至setTimeout中的代码也是可以批量执行的,而且为了兼容react17这种特殊的Processing,在并发模式未开启的情况下,即仍然使用ReactDOM.render的API时,executionContext会是不指定executionContext立即渲染:react18普及后,所有setStates都会被批处理,不会出现批处理或非批处理的Bulk问题。总结虽然我们讨论的是同步和异步setState,但这并不是setTimeout和Promise的异步。只是指setState后状态是否立即改变,是否立即渲染。我们梳理了React的渲染流程,包括render阶段和commit阶段。渲染阶段是从vdom到fiber,包括schedule和reconcile。commit阶段是将fiber更新为dom。渲染过程的入口点是performSyncWorkOnRoot函数。setState会创建一个update对象挂在fiber对象上,然后调度performSyncWorkOnRoot重新渲染。在react17中,setState是分批执行的,因为executionContext是在执行前设置的。但如果是在setTimeout、事件监听等函数中,则不会设置executionContext,此时会同步执行setState。可以在外面包裹一个batchUpdates函数,手动设置executionContext切换到异步批量执行。在react18中,如果使用createRootapi,就不会出现这个问题。react18普及后,setState是同步还是异步的问题将不复存在,因为所有的setState都是异步批量执行的。