面试官:“宝子,setState 是同步还是异步的呀?”

2,381 阅读6分钟

这一次,我将带你一次性搞懂 React 中常见的 setState 原理。

setState 本身的默认行为

在进入主题之前,你肯定需要先学会 React 的基本使用。如果不会,请点赞离开;如果会用 React ,那就点赞收藏后离开(●'◡'●)。

我们在使用 React 的时候,经常会用到 state(一句废话),但是真正能完全搞清楚 setState 的帅哥美女,确实没几个。毕竟程序员都不太可能像我一样博学(和好看)。那么,要搞清楚它,应该去投胎(整容)吗?

不,你需要先搞清楚 setState 本身的默认行为

其实也很简单,我们都知道,setState可以传递对象形式的状态,也可以传递函数形式的状态。而不论状态是对象形式还是函数形式,它都会先将所有状态保存起来,然后进行状态合并,所有状态合并完成后再进行一次性 DOM 更新。

如果状态是对象形式,后面的状态会直接覆盖前面的状态。类似于 Object.assign() 的合并操作。

对于对象状态这一点,我们有请翠花,上代码:

运行代码,Dom 中展示的结果为 1。很显然两次 setState 只有一次生效了。

真的吗?其实两次都有生效,只不过这两次 setState 在执行前,被合并成了一个。你不能说到底是那个生效,你可以说两个都没生效,因为最终执行的是被合并的那个代码

如果状态是函数形式,那么依次调用函数进行状态累积,所有函数调用完成后, 得到最终状态,最终进行一次性 DOM 更新。

翠花,再来一段代码……

明显不一样的结果就能说明,两次都执行了,因为函数状态并不会合并,而是以此运行。

好了,翠花可以先下去休息了,前置只是我们已经梳理完了,那么,对于 setState 的研究就结束了吗?当然不是,接下来,让我们换个场子,继续掰滔(battle)。

setState 同步 OR 异步

在面试场景中,只要和 React 相关,面试官一定舔着脸问你:“ 宝子,setState 是同步还是异步的呀 ” 。

面对这样的无耻刁难,我们需要先明确,从 API 层面上说,它就是普通的调用执行的函数,自然是同步 API 。

因此,这里所说的同步和异步指的是 API 调用后更新 DOM 是同步还是异步的。

来,我们有请娜塔莎,上代码……

果然,洋妹子端上来的代码确实不好消化,通过结果我们发现,非常奇怪的一个现象:

第一次事件执行显然为异步的,先打印了两个 0,Dom 随之改变为 1 ;

第二次同样是异步的,但是我们发现多次执行没效果 (异步?);

而第三次又是同步执行的了;

这什么情况,洋妹子给我们下了迷药吗?看我葵花宝典戳破它。

先说结论,首先,同步和异步主要取决于它被调用的环境

  • 如果 setState 在 React 能够控制的范围被调用,它就是异步的。比如合成事件处理函数, 生命周期函数, 此时会进行批量更新, 也就是将状态合并后再进行 DOM 更新。

  • 如果 setState 在原生 JavaScript 控制的范围被调用,它就是同步的。比如原生事件处理函数中, 定时器回调函数中, Ajax 回调函数中, 此时 setState 被调用后会立即更新 DOM 。

为什么会这样呢?

其实,我们看到的所谓的 “异步”,是开启了 “批量更新” 模式的。

批量更新模式可以减少真实 DOM 渲染的次数,所以只要是 React 能够控制的范围,出于性能因素考虑,一定是批量更新模式。批量更新会先合并状态,再一次性做 DOM 更新。

那么假设没有批量更新呢?

从生命周期的角度来看,每一次的 setState 都是一个完整的更新流程,这里面就包含了重新渲染 (re-render) 在内的很多操作,大体的流程如下:

shouldComponentUpdate->componentWillUpdate->render->componentDidUpdate;

re-render 本身涉及对 DOM 的操作,它会带来较大的性能开销。假如说 “一次 setState 就触发一个完整的更新流程” 这个结论成立,那么每一次 setState的调用都会触发一次 re-render,我们的视图很可能没刷新几次就卡死了,渲染就会出现下面这样的流程:

因此,setState 异步(或者说是批量更新)的一个重要动机就是避免频繁的 re-render

在实际的 React 运行时中,setState 异步的实现方式有点类似于浏览器里的 Event-Loop:

每来一个setState,就把它塞进一个队列里。等时机成熟,再把队列里的 state 结果做合并,最后只针对最新的 state 值走一次更新流程。

这个过程,叫作“批量更新”,批量更新的过程正如下面代码中的箭头流程图所示:

只要我们的同步代码还在执行,“进队列” 这个动作就不会停止。因此就算我们在React 中写了一个 N 次的 setState 循环,也只是会增加 state 任务入队的次数,并不会带来频繁的 re-render。当 N 次调用结束后,仅仅是 state 的任务队列内容发生了变化, state 本身并不会立刻改变。

为了更好地让你吃下娜塔莎,哦不对,是娜塔莎端上来的美食,我帮你梳理了 setState 的执行流程图:

当然,你可能看不懂这个流程图(是有多笨啊),没关系,下面还会有的。

如果为非批量更新模式,调用多少次 setState 就会渲染多少次真实 DOM,性能较低。

但是我们在某些条件下需要对 JS 控制的区域实现批量更新 ( 异步更新 DOM ) ,那应该怎么做呢?

强制批量更新

其实很简单,我都不好意思说 so easy ,因为这玩意简直就是 so TM 的 easy 。

我们只需要将代码包裹在 unstable_batchedUpdates 方法的回调函数中就可以实现强制批量更新。

具体使用方式也很简单,从 react-dom 中引入进来,然后将代码放入调用函数中就可以了。

(翠花和娜塔莎结婚了,我来给大家上代码)

截止到现在,我们成就了一对完美的爱情,啊,呸~

未完待续...