为什么在React 中setState 是“异步”的

浏览量:358

前言

对于熟悉其他框架但首次使用React 的初学者而言,会发现一个奇怪的问题:setState(class 中使用this.setState 或 在hooks 中使用setState function)被调用之后,去获取state 的值仍然没有发生改变,但有时候,调用setState 后访问state 值时又会发现state 值会立即被更新(比如在React 16 中在setTimeout 内使用setState)。

这一点会让人非常困惑,甚至还衍生出了面试题:高频面试题:setState是同步还是异步

这里的异步只是概念上的异步,用来表示state 没有立即更新的意思。事实上setState 当然是同步调用。

造成这种现象的原因

这个问题的原因,不仅困扰着诸多开发者,连周边生态库的作者也很疑惑。

Mobx 的作者Michel Weststrate 在17年就在React 仓库提了issue, 这个问题也得到了React 核心成员Dan Abramov 的解答。

英文好的同学可以直接阅读issue, 英文不太好的同学可以阅读 这里

或者可以看一看文档对与state 的介绍:

这里再简单总结一下这样做的原因:

  1. 批处理更新是必要的,即render 并非是在状态更新就进行,而是等待所有状态都更新完成再进行render,这一周期内只render一次。(vue 也有类似的机制,比如nextTick)
  2. 我们可以选择让setState 立即更新state,但一些原因让我们没有选择这样的方案:
    a. 保持状态的一致性,让props、state、ref 保持一致
    b. 如果state 立即更新了,Concurrent Render 就无法进行了,因为它就是要去对所有的render 进行调度

批处理更新 (batchUpdates) 很容易理解,但关于为什么没有选择立即更新,我觉得我有必要再呈上一点浅见。

状态保持一致

状态保持一致是非常有必要的,试想一下当父组件与子组件对于同一值取到的数据不一致是有多么的割裂?

在react 中,更新props 的唯一方式就是父组件的重新render,所以要状态保持一致的话就必须在state 更新时重新去render 组件。

而对于render 来说,Batch update 是框架设计者必须考虑的方案,这样可以大幅减少无用的render 及操作真实DOM 的次数。参考以上条件,立即更新state 与props 就无法使用Batch update。所以Vue 也有类似Batch update的机制,Vue 称其为异步更新队列

同样的,Vue 在子组件更新父组件传过来的值时,也并非是立即更新的,和react 需要保持状态一致的原因大致相似。

Vue 的示例可见这个demo,需要打开log 或浏览器控制台,点击文字可以看到props 的更新也并非一致的:

log

为什么Concurrent Render 中不能立即更新值

在Concurrent Render 下,所有的state 更新会被调度器 ( Scheduler ) 接管。换句话来说,你只是标记了这个状态会被更新(实际react 也只是留下了一个标记表示这个变量将变化),实际上什么时间进行更新是由调度器根据不同情况下的优先级来决定的,调度器甚至会跳过优先级较低的更新或者将某些密集更新通过时间分片来分n次来完成。

这样的话更不可能立即拿到更新后的值了。

使用react 时,我们应该怎么做

无论工具的作者怎么解释,反直觉的就是反直觉的,但只要对机制熟悉了,绝大多数开发过程中我们都不会遇到让人困惑的问题。

当使用Class 组件时,this.setState 方法提供了callback 方法,类似与Vue 的$nextTick,在callback 中你可以获取到state 已被更新的值。

困惑大多发生于使用Function 组件时,Function 组件的setState 方法并没有提供callback,因为Function 组件的副作用应该交给useEffect 去处理。

如果真的遇到需要立即拿到state 值的情况,请按照以下逐步处理一下:

1. 这个值是需要放在state 中来维护的吗?(这个值与UI 有关联吗)
2. 是否已经确切的知道新state 的值是什么而直接使用?
3. 使用ref 来代替state 管理这个值,ref 是引用值,可以跨越组件渲染,保证获取到的总是最新值。

题外话 – 为什么某些状态管理库的状态更改并不是这样的? 比如Mobx

实际上当Mobx 中的值作为props传入子组件时表现是与state一致的。

但由于Mobx 的特性,大家更习惯于直接去从Mobx store 中获取值而非使用props 来传导。

而只在组件中作为state使用时,类似于使用ref,当访问值时访问的是某个引用,变量自然是最新值了。

另一方便,Mobx 似乎到现在(2022/6/29 )还不能支持Concurrent Render , 作者曾在20年 提了一个思路,但仍不清楚Mobx 团队的最终会如何解决这个问题。

留下评论