面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

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

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

setState 本身的默认行为

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

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

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

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

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

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

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

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

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

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

翠花,再来一段代码……

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

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

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

setState 同步 OR 异步

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

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

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

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

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

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

  • 第一次事件执行显然为异步的,先打印了两个 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原理,你来说说是同步还是异步的?”

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

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

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

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

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

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

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

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

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

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

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

强制批量更新

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

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

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

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

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

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

我们基本搞清楚了原理流程,那具体的代码是如何实现的呢?

在 setState 的调用中,有一个合成事件起到了关键性的作用。接下来,我们先去搞清楚这个小可爱,再来看具体的 setState 的代码实现。

合成事件

首先明确定义,在 React 中为元素添加的事件被叫做合成事件。

合成事件的好处有两个:

  • 一是屏蔽了浏览器之间关于事件处理的兼容性问题,为合成事件对象内部提供了统一的 API;
  • 二是性能的提升, 事件都被委托给 document 。

React 并不会将事件添加到真正的 DOM 元素身上,它会将所有事件委托给 document 执行。如下图所示:

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

React 会在拥有事件的 DOM 对象身上添加一个 store 对象,在 store 对象中存储事件名称及事件处理函数,然后通过 document 分发事件。

当事件被触发后,通过获取事件源对象,查看事件源对象中是否存在 store 对象,获取 store 对象中事件处理函数,执行事件处理函数。

合成事件的事件对象在使用完成以后会被销毁。我长得帅,所以,我写了一段模拟性的代码,你看不看就随意了:

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

代码的注释中说的已经非常清楚了,爱看不看吧,就这样……

setState 实现原理

接着,我们再把上面的图拿出来,我分为了四段,进行了具体的梳理。先看图,再看字,最后上代码 ?

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

1、当 setState 方法被调用后,方法会将状态传递给组件更新器,让组件更新器将状态临时存储起来。每个组件都会有自己的组件更新器,当需要更新组件时调用组件更新器。

2、状态临时保存完成后判断当前是否为批量更新模式,如果是,将组件更新器添加到更新队列中;如果不是,直接更新组件。

批量更新模式是如何设置的:当触发合成事件时, 在事件处理函数执行之前,会先将批量更新模式设置为 true,然后执行事件处理函数收集状态。当事件处理函数执行完成后,执行批量更新操作,即从更新队列中获取组件更新器并调用。组件更新器调用完成后再将批量更新模式设置为 false。

3、更新组件时,先判断是否有状态需要更新,如果有就先计算最新状态,将得出的最新状态重新设置给组件。

计算状态时,如果状态是函数类型,调用函数传入当前状态,返回最新状态。如果状态是对象类型,使用对象状态覆盖原有状态。

4、组件状态计算完成后,通过调用组件内部的 render 方法获取新的 VirtualDOM,再通过 DOM 对象获取旧的虚拟 DOM,然后调用 diff 方法进行比对,对比完成后将差异更新到真实 DOM 对象中。

下面的代码,就是配合前面的流程图和文字描述实现的具体代码了。我看你也挺聪明的,所以加了非常详细的注释,就是为了遏制你的智力增长,如果还看不懂,那就算了吧,建议你去和娜塔莎抢婚,单身程序员这条路可能不适合你哟 (●'◡'●)。

面试官:“年轻人,我看你很懂setState原理,你来说说是同步还是异步的?”

好了,就到这里吧。没想到你竟然真的没看代码,就知道往下拖进度条,哎,放弃吧,翠花是娜塔莎的,不可以插足!

当然,如果能点赞,我可以考虑送你一只翠花┗|`O′|┛。

本文作者:北瑶

系统架构师,技术专家,全栈工程师,拥有丰富的技术研发经验。 目前在某互联网大厂任前端技术专家,负责前端架构研发。针对前端工程化、Serverless 架构、微前端架构、服务端渲染等技术进行落地实施。

著有《Node.js工程师养成计划》,保姆级教程,手把手带你完成工具、服务器、中间层等多类应用开发实战。

发表评论

相关文章