V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
dcsuibian
V2EX  ›  React

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

  •  
  •   dcsuibian · 352 天前 · 5155 次点击
    这是一个创建于 352 天前的主题,其中的信息可能已经有所发展或是发生改变。

    以下代码

    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>
        )
    }
    

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

    image-20240105095352796

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

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

    56 条回复    2024-01-05 18:29:45 +08:00
    3059349417
        1
    3059349417  
       352 天前
    你没学过编程么,异步不懂?
    MorningStar0
        2
    MorningStar0  
       352 天前
    调用 prev1 的时候组件更新然后后面的 set 函数都会进入等待更新队列,这是时候执行 console 所以输出了 state 。后面的就进行合并更新了。对于合并更新参考 https://juejin.cn/post/7228851979462737978
    vincenteof
        3
    vincenteof  
       352 天前   ❤️ 1
    react.dev 先读一遍
    dcsuibian
        4
    dcsuibian  
    OP
       352 天前   ❤️ 3
    @3059349417 你有看我的内容吗? prev1 这里明显是被同步的调用了(虽然状态没更新),而其他的都是异步的。
    chenliangngng
        5
    chenliangngng  
       352 天前
    state 1 那是因为这里是个闭包,底层的 A 其实已经改了
    bojackhorseman
        6
    bojackhorseman  
       352 天前   ❤️ 2
    上面说的很清楚啦。
    看过一条推文大意是很多人懂 js 所以就下意识以为也懂 React ,实际上并没有仔细看过文档。一开始写 react 我也是,结果踩了一堆坑。比如 useEffect 和我以为的 vue watch 根本不一样,还是写 vue 顺手
    codehz
        7
    codehz  
       352 天前 via iPhone
    除了 react 有意将副作用隔离之外还有一个就是语言限制,即你不能修改一个被解构出来的变量(虽然就算你不解构它也不会变)
    dcsuibian
        8
    dcsuibian  
    OP
       352 天前   ❤️ 1
    @chenliangngng 我不是关心这个值,我是关心为什么这个函数是立即执行了
    zackzergzeng
        9
    zackzergzeng  
       352 天前
    要先把 react 这个概念先搞懂(范志毅脸)
    Bijiabo
        10
    Bijiabo  
       352 天前
    1. 打开 Google
    2. 找到输入框,输入“React useState 原理”
    3. 按下回车
    4. 阅读文章
    angrylid
        11
    angrylid  
       352 天前 via Android
    这个应该是 stale closure
    ivslyyy
        12
    ivslyyy  
       352 天前
    ivslyyy
        13
    ivslyyy  
       352 天前
    @dcsuibian

    Here, n => n + 1 is called an updater function. When you pass it to a state setter:

    React queues this function to be processed after all the other code in the event handler has run.
    During the next render, React goes through the queue and gives you the final updated state.
    wgbx
        14
    wgbx  
       352 天前
    setA 是函数,他映射的 useState 函数是异步函数,console.log('A', 'state', A)展示 1 ,有啥问题?
    clue
        15
    clue  
       352 天前
    所以, 你是对时序有要求?

    如果是我设计的话, 考虑有个 effect B 依赖 A, 那我修改 A 多次, 这个 effect B 是否要执行多次呢? react 重新执行 hook, 我记得是需要再次执行 render 函数的, 碰到你这个场景, 如果 setState 时正在执行 render, 要怎么处理呢?

    总之这种 依赖 -> 响应 的模式很脆弱, 为了防止出问题, 会加很多限制与异步
    dcsuibian
        17
    dcsuibian  
    OP
       352 天前   ❤️ 1
    @wgbx 我对他打印的值一点问题都没有,我有问题的是为什么打印的是:
    A prev1 1
    A state 1
    A prev2 2
    A prev3 3
    而不是:
    A state 1
    A prev1 1
    A prev2 2
    A prev3 3
    Imindzzz
        18
    Imindzzz  
       352 天前 via Android
    经典面试题“setState 是异步还是同步”
    swaggeek
        19
    swaggeek  
       352 天前   ❤️ 1
    @dcsuibian 设值这个过程是一个同步的过程吧,需要执行函数,然后拿到新值,所以立刻执行了,后面的没有立即执行,是因为像 2 楼说的,进入了批量更新的队列,被加了锁了。需要等这次更新完,才能执行,有点像微任务队列吧。
    barbara012
        20
    barbara012  
       352 天前   ❤️ 6
    怎么感觉大多都没 get 到 op 困惑点?
    visper
        21
    visper  
       352 天前   ❤️ 4
    可能题主的奇怪点在于,如果那些函数都是放在队列里面等时机再执行,那应该最后的 A state1 就会在 A prev 1 前面。这样卡在中间的话不知道 react 是什么执行规则的。
    JounQin
        22
    JounQin  
       352 天前
    不要学八股文
    codehz
        23
    codehz  
       352 天前 via iPhone
    要不你试试不同版本的区别(我感觉这应该有差异
    codehz
        24
    codehz  
       352 天前 via iPhone
    有没有一种可能和 Fiber Reconcilier 的调度有关系
    oatw
        25
    oatw  
       352 天前   ❤️ 1
    感觉很多兄弟没审题就质疑楼主呢?

    没看过这部分源码,盲猜可能是这样:

    1. 第一次调用 set 的时候通知 react 要更新了;
    2. 在准备 rerender 前,其它 set 操作会被入队,等待一起执行;
    3. 执行所有队列中的 set 操作&rerender 。

    我觉的关键就是 react 总得知道要更新这件事,而这里就是第一次 set 触发的,里面的函数也真的执行了,然后在真正更新前发现还有其他 set ,所以就入队了其他 set 操作。
    chenliangngng
        26
    chenliangngng  
       352 天前
    @dcsuibian 在触发更新前,变更函数都会立即执行,因为只有执行了 react 才知道你是否变更,你返回相同值试试。在发生变更之后,剩下的变更函数全部打到更新队列里去
    oatw
        27
    oatw  
       352 天前   ❤️ 1
    @oatw 补充一下,什么时候执行 set 内函数其实并不是关键,即便执行了,也只是计算了结果暂存下来,并没有真的更新状态变量,状态变量都是在队列中所有函数执行后的最终计算结果下更新。但是私以为第一次 set 的时候内部的函数也可以不立刻执行,而是放到队列中和其他的一起执行,至于为啥,只能读源码了。官方文档也不完全可信。
    johnnyNg
        28
    johnnyNg  
       352 天前   ❤️ 1
    react 的 hooks 确实很反直觉,我只喜欢 react'的 jsx ,vue'的 watch 、computer 就感觉很符合直觉
    swaggeek
        29
    swaggeek  
       352 天前
    @oatw 我看了下 React 文档,他是需要在 render 的时候再去看看队列里有没有 set 函数,然后再执行取值的。感觉这样其实 react 会有很多次无效渲染。像 Vue 就有合并状态的这种操作,最终只会更新一次。
    ljpCN
        30
    ljpCN  
       352 天前
    [今天让你彻底搞懂 setState 是同步还是异步]( https://zhuanlan.zhihu.com/p/350332132)
    swaggeek
        31
    swaggeek  
       352 天前
    @swaggeek 我没怎么使用过 react hook ,我是写 Vue 的,不过我感觉 react 这样的设计还是挺奇怪的。
    oatw
        32
    oatw  
       352 天前
    @swaggeek 啊!我不知道呀。纯粹是根据经验盲猜的。React 和 Vue 我都没兴趣,前端太难了,我不配,哈哈哈~
    helIo0o
        33
    helIo0o  
       352 天前   ❤️ 1
    用同样的代码试了一下,
    第一次点击按钮时,打印顺序:
    A prev1 1
    A state 1
    A prev2 2
    A prev3 3
    A prev4 4
    A render 5

    后面再点击按钮时,打印顺序:
    A state 5
    A prev1 5
    A prev2 6
    A prev3 7
    A prev4 8
    A render 9

    prev1 那里只在第一次点击的时候,才会同步执行一次,后面都是异步的了
    dcsuibian
        34
    dcsuibian  
    OP
       352 天前
    @helIo0o 确实,这个我倒没注意到。
    mikami
        35
    mikami  
       352 天前   ❤️ 1
    [react-hooks-usestate-set-function-exhibits-sync-as-well-as-async-behaviour]( https://stackoverflow.com/questions/72200664/react-hooks-usestate-set-function-exhibits-sync-as-well-as-async-behaviour)

    真想要搞明白为什么这个顺序,我估计只能看源码了。只能说日常工作中不要这么写
    wpzz
        36
    wpzz  
       352 天前
    "
    If something is offscreen, we can delay any logic related to it. If data is arriving faster than the frame rate, we can coalesce and batch updates. We can prioritize work coming from user interactions (such as an animation caused by a button click) over less important background work (such as rendering new content just loaded from the network) to avoid dropping frames.

    To be clear, we are not taking advantage of this right now. However the freedom to do something like this is why we prefer to have control over scheduling, and why setState() is asynchronous. Conceptually, we think of it as “scheduling an update”.
    "

    如果某些内容在屏幕外,我们可以延迟与其相关的任何逻辑。如果数据到达速度快于帧速率,我们可以合并并批量更新。
    Leviathann
        37
    Leviathann  
       352 天前
    react 是协程式的执行方式,不要依赖它调用顺序
    Chingim
        38
    Chingim  
       352 天前
    好问题, 值得写一篇文章
    vincenteof
        39
    vincenteof  
       352 天前
    这应该是实现细节,说不定哪个版本以后就变了。updater 如果是 pure function 的话,你可能不需要关心它到底什么时候被执行?
    wpzz
        40
    wpzz  
       352 天前
    @wpzz 是这里吧,filber 并发更新。

    // If a render is in progress, and we receive an update from a concurrent event,
    // we wait until the current render is over (either finished or interrupted)
    // before adding it to the fiber/hook queue. Push to this array so we can
    // access the queue, fiber, update, et al later.
    const concurrentQueues: Array<any> = [];
    let concurrentQueuesIndex = 0;

    let concurrentlyUpdatedLanes: Lanes = NoLanes;


    而且开发模式下不太准, 源码中有很多地方判断了__DEV__
    Pencillll
        41
    Pencillll  
       352 天前   ❤️ 6
    https://github.com/reactjs/react.dev/issues/5982#issuecomment-1674183863

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

    循环 setState 只需要纳秒单位的时间,理论上只会显示最新值,实际上如果机器配置足够低,是能看到中间值的。
    Alander
        45
    Alander  
       352 天前   ❤️ 2
    似乎楼内没有回答到点子上的,去 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
        46
    Alander  
       352 天前
    具体代码看 ReactFiberHooks.js 里的 dispatchSetState 方法吧,还是比较清晰明了的,日常还遇到过很多奇葩的问题其实去看一眼源码就大概了解了,react 还是比较线性的
    Mikawa
        47
    Mikawa  
       352 天前
    OP 的 React 是哪个版本的

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

    TLDR;
    因为性能优化, 如果当前 fiber 没有更新, react 会先执行 setState 的回调, 用来做性能优化: 如果 setState 的结果和当前值一致, 就跳过后续的 reconcile 和 render 过程.
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2877 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 80ms · UTC 12:31 · PVG 20:31 · LAX 04:31 · JFK 07:31
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.