搞不懂 useState 为什么会这么设计?

2024-01-05 09:59:09 +08:00
 dcsuibian

以下代码

import {useState} from "react";

export default function StateUpdate() {
    const [A, setA] = useState(1)
    console.log('A', 'render', A)
    return (
        <div>
            <button onClick={() => {
                setA(prev => {
                    console.log('A', 'prev1', prev)
                    return prev + 1
                })
                setA(prev => {
                    console.log('A', 'prev2', prev)
                    return prev + 1
                })
                setA(prev => {
                    console.log('A', 'prev3', prev)
                    return prev + 1
                })
                setA(prev => {
                    console.log('A', 'prev4', prev)
                    return prev + 1
                })
                console.log('A', 'state', A)
            }}>
                button A {A}
            </button>
        </div>
    )
}

点击一下按钮后的执行结果是什么?

A state 1正正好好卡在中间,虽然此时状态还是 1 。但说明了 prev1 这个函数是立即执行的。

我真的搞不懂为什么要这么设计?

5177 次点击
所在节点    React
56 条回复
Pencillll
2024-01-05 12:39:25 +08:00
https://github.com/reactjs/react.dev/issues/5982#issuecomment-1674183863

> The timing for when the callback is called is not guaranteed.
charlie21
2024-01-05 12:50:58 +08:00
在点击事件之后最新渲染值,Vue 和 React 的这个行为各是出于什么考虑?
https://www.zhihu.com/question/543057656
realJamespond
2024-01-05 13:41:20 +08:00
估计是 setState 第一次直接执行,第二次以后放入队列?这样强行解释满意不😂
ragnaroks
2024-01-05 13:42:08 +08:00
react fiber 更新会“自己”选择每批执行的逻辑,同步也好异步也好没法在代码层面保证执行时机,这个设计的目的是确保在大多数情况下不会低于 60 帧,实际上还是看编码人员的水平。

循环 setState 只需要纳秒单位的时间,理论上只会显示最新值,实际上如果机器配置足够低,是能看到中间值的。
Alander
2024-01-05 14:24:40 +08:00
似乎楼内没有回答到点子上的,去 github 拉一下 react 源码就知道为什么了,useState 返回的数组的第 1 个对象是个 dispatch 方法,该方法伪代码如下:
```js
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update)
} else {
doAction()
enqueueRenderPhaseUpdate(queue, update)
}

```
第一次 setState 是在 else 里面会调用 action 就是打印 A prev1 1 ,后续的几次其实因为本次渲染应该在微任务队列所以走到 if 里面
Alander
2024-01-05 14:26:23 +08:00
具体代码看 ReactFiberHooks.js 里的 dispatchSetState 方法吧,还是比较清晰明了的,日常还遇到过很多奇葩的问题其实去看一眼源码就大概了解了,react 还是比较线性的
Mikawa
2024-01-05 14:26:53 +08:00
OP 的 React 是哪个版本的

这里 onClick 是个合成事件的回调,可能是受到 Reconcilier 的影响了,用 addEventListener 试试呢
mxT52CRuqR6o5
2024-01-05 14:28:11 +08:00
严格模式且开发模式下 setState 后会执行一次 render 检查用户的 useEffect 是否有正确清除副作用
mxT52CRuqR6o5
2024-01-05 14:29:04 +08:00
AQingC
2024-01-05 14:42:02 +08:00
楼主 react 版本是多少?
royzxq
2024-01-05 14:48:22 +08:00
这和 fiber 的实现有关.. 具体的话可能要看源码
rrfeng
2024-01-05 14:57:57 +08:00
楼上有人回答了,我半吊子 js/react 水平,但是我知道
setState 和 console.log 都是异步的,所以我不能相信任何它的顺序。
xuhai951753
2024-01-05 15:34:00 +08:00
如果你想问为什么这么设计,就是为了避免大量的渲染,否则每次 set 都会 render 一遍。你可以在 react 非 concurrent 模式下将 onClick 的内容在 setTimeout 中执行做对比。
如果你想问实现,简单来说在合成事件或者 concurrent 模式中第一次 set 之后就会打个标,标记要 update ,后续的所有 set 就会合并到更新时执行,至少是下一个时间切片。
HTML001
2024-01-05 15:36:58 +08:00
笑了,前面几楼一上来就嘲讽,但是连问题都没看明白
zangbianxuegu
2024-01-05 15:43:50 +08:00
@Pencillll 大佬是怎么快速搜索/定位到问题解释的呢?
Chingim
2024-01-05 18:29:45 +08:00
写了篇文章分析: https://www.overcch.com/posts/useState-callback-timing

TLDR;
因为性能优化, 如果当前 fiber 没有更新, react 会先执行 setState 的回调, 用来做性能优化: 如果 setState 的结果和当前值一致, 就跳过后续的 reconcile 和 render 过程.

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1006034

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX