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

刚学习 React 请教一个 useState 有关的问题

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

    代码如下:

    import React from 'react';
    import { useState, useEffect } from 'react';
    
    export default function App() {
      const [arr, setArr] = useState([0]);
    
      useEffect(() => {
        console.log(arr);
      }, [arr]);
    
      const handleClick = () => {
        Promise.resolve()
          .then(() => {
            setArr(prevState => [...prevState, 1]);
          })
          .then(() => {
            Promise.resolve()
              .then(() => {
                setArr(prevState => [...prevState, 2]);
              })
              .then(() => {
                setArr(prevState => [...prevState, 5]);
              })
              .then(() => {
                setArr(prevState => [...prevState, 6]);
              });
          })
          .then(() => {
            setArr(prevState => {
              setArr(prevState => [...prevState, 4]);
              return [...prevState, 3];
            });
          })
          .catch(e => {
            console.log(e);
          });
      };
    
      return (
        <>
          <button onClick={handleClick}>change</button>
        </>
      );
    }
    
    

    点击按钮后,控制台的结果显示为

    (1) [0]
    (2) [0, 1]
    (5) [0, 1, 2, 3, 4]
    (6) [0, 1, 2, 3, 4, 5]
    (7) [0, 1, 2, 3, 4, 5, 6]
    

    想知道为啥结果不是[0, 1, 2, 5, 6, 3, 4],以及如何在 Promise 链中正确调用 setState,感谢大家的指点

    第 1 条附言  ·  354 天前
    附上代码的在线执行地址吧: https://playcode.io/1690189
    第 2 条附言  ·  353 天前
    • 感谢大家的指导和回复
    • 确实这应该是一个 Promise 的问题而不是 useState的问题,标题有点误导大家了
    • 我本人是个前端小白,学习 useState 时看到了这种用法可以解决我遇到的问题(出自 React Hook 中 useState 异步回调获取不到最新值及解决方案 ) 结果测试过程中写出了这段诡异的代码,hhh
    16 条回复    2023-12-11 18:00:24 +08:00
    jaylee4869
        1
    jaylee4869  
       354 天前
    setState is an async function.
    okakuyang
        2
    okakuyang  
       354 天前
    你第二个 then 开始就有问题了
    whoami9426
        3
    whoami9426  
    OP
       354 天前
    @jaylee4869 这个我知道,但是为啥和 then 结合在一起感觉没有达到编码的预期效果
    whoami9426
        4
    whoami9426  
    OP
       354 天前
    @okakuyang 是 Promise 嵌套的问题吗?
    whoami9426
        5
    whoami9426  
    OP
       354 天前
    附上代码的在线执行地址吧: https://playcode.io/1690189
    common0
        6
    common0  
       354 天前   ❤️ 1
    第二个 then() 里的 Promise.resolve() 前加上 return ,5 、6 就在 3 、4 前面了。
    wipbssl
        7
    wipbssl  
       354 天前   ❤️ 2
    异步逻辑的问题,第二个 then 相当于 async 函数里放了一个 aync 函数,但没有加入 await ,所以执行到 setArr 2 时就异步,主线程继续执行第三个 then 了,5 和 6 要等到 2 的 promise resolve 后才会执行,但任务序列中第 3 个 then 在 setArr 2 resolve 前就加入队列了
    chenliangngng
        8
    chenliangngng  
       354 天前   ❤️ 1
    Promise 立即执行,当前轮宏任务
    then 当前轮微任务
    useState 浏览器空闲,也就是上一轮执行完了,下一轮宏任务
    then 下一轮的微任务
    onec
        9
    onec  
       354 天前
    第一个 Promise.resolve()后立即执行 then1 回调,then1 返回 pending promise ,then2, then3 依次进入队列
    then2 回调开始执行,then2.1 立即执行返回 pending promise, then2.2, then2.3, then2.3 进入队列
    then3 执行
    then2.2 执行
    then2.3 执行
    done
    SilencerL
        10
    SilencerL  
       354 天前   ❤️ 1
    简化一下你的代码:

    Promise.resolve()
    .then(() => {
    setTimeout(() => console.info(1))
    })
    .then(() => {
    Promise.resolve()
    .then(() => {
    setTimeout(() => console.info(2))
    })
    .then(() => {
    setTimeout(() => console.info(5))
    })
    .then(() => {
    setTimeout(() => console.info(6))
    });
    })
    .then(() => {
    setTimeout(() => {
    console.info(3)
    Promise.resolve().then(() => console.info(4))
    })
    })

    进一步简化:

    Promise.resolve()
    .then(() => {
    console.info(1)
    })
    .then(() => {
    Promise.resolve()
    .then(() => {
    console.info(2)
    })
    .then(() => {
    console.info(5)
    })
    .then(() => {
    console.info(6)
    });
    })
    .then(() => {
    console.info(3)
    // console.info(4) // 1 2 3 4 5 6
    // Promise.resolve().then(() => console.info(4)) // 1 2 3 5 4 6
    // setTimeout(()=>console.info(4)) // 1 2 3 5 6 4
    })

    你这个问题可以简化成和 React 没任何关系的问题,纯粹是浏览器任务队列的问题,Promise.resolve().then 可以生成一个微任务,setTimeout 或者你问题中的 setArr 生成的时宏任务(现代浏览器没得宏任务了,分成了更多任务列表,但是为了方便解释就还是说宏任务)

    你可以观察进一步简化后的版本,以及看一下最后一个 then 里面关于不同方式 4 的输出时机,尝试理解一下。

    但是不得不说,理解起来可能很困难,你需要了解 js 的事件循环以及队列优先级的问题。

    大概来说,微任务优先级高,宏任务优先级低,每次事件循环按照优先级拿一遍任务。

    - 最顶层的 Promise.resolve().then -> console.info(1) 立刻输出 1

    - 第二个 then
    -- 第二个 then 里面 Promise.resolve().then -> console.info(2) 立刻输出 2
    -- 第二个 then 里面 Promise.resolve() 的第一个 then 作为一个微任务已经结束,后续的第二个 then 扔到下一次微任务队列中

    - 第三个 then
    -- 第一句 console.info(3) 立刻输出 3
    -- 第二句:
    --- 情况 1:console.info(4) 那就立刻输出 4
    --- 情况 2:Promise.resolve().then(() => console.info(4)) 扔一个微任务到下次事件队列,任务是 console.info(4)
    --- 情况 3:setTimeout(()=>console.info(4)) 扔一个宏任务到下次事件队列,任务是 console.info(4)

    - 下一次循环
    -- 微任务队列有一个
    .then(() => {
    console.info(5)
    })
    .then(() => {
    console.info(6)
    });
    --- 这里你可以看成一个新的
    Promise.resolve()
    .then(()=> console.info(5))
    .then(() => {
    console.info(6)
    });
    (当然这不能真的这么看,但是为了讲解方便,你就这么理解好了。。。)
    --- 所以立即执行了这个微任务 console.info(5),把 console.info(6) 继续扔到微任务队列
    -- 如果上一次循环的第三个 then 里面情况 2 ,那么在上一步的 5 输出结束后 6 的前面就有一个上一次扔过来的的微任务:console.info(4)
    -- 如果上一次是情况 3 ,输出 4 这个任务在宏任务队列,那么就先不管他,把当前下一个微任务输出 6 执行,再去执行宏任务队列 console.info(4)

    单纯文字讲的讲不清楚,你要实际用代码多试几次,尽可能简化代码并且尝试不同的 case ,才能大概理解这个幺蛾子事件队列

    顺便感谢你提供一道好玩的面试题,下次可以拿去为难其他人(
    otakustay
        11
    otakustay  
       354 天前
    你这代码就是改成 console.log 也是 0, 1, 2, 3, 4, 5, 6 吧,和 state 一点关系都没有,纯 Promise 执行顺序问题
    8XIQz5SCHX1U6c7s
        12
    8XIQz5SCHX1U6c7s  
       353 天前
    好好好,又复习了一遍事件循环
    lilei2023
        13
    lilei2023  
       353 天前
    这和 setState 没关系吧,这不就是 promise 执行问题么
    lilei2023
        14
    lilei2023  
       353 天前
    @lilei2023 不过我也做错了
    chanChristin
        15
    chanChristin  
       353 天前
    有意思,所有的 ai 都认为答案是 [0, 1, 2, 5, 6, 3, 4]
    CrispyNoodles
        16
    CrispyNoodles  
       353 天前
    好好好,这样玩是吧。本来写完代码只是想摸下鱼,强制让我复习了一遍事件队列,内存,栈,堆的概念
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1468 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 17:24 · PVG 01:24 · LAX 09:24 · JFK 12:24
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.