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

2023-12-10 13:33:17 +08:00
 whoami9426

代码如下:

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,感谢大家的指点

3074 次点击
所在节点    React
16 条回复
jaylee4869
2023-12-10 13:42:35 +08:00
setState is an async function.
okakuyang
2023-12-10 13:50:29 +08:00
你第二个 then 开始就有问题了
whoami9426
2023-12-10 13:56:16 +08:00
@jaylee4869 这个我知道,但是为啥和 then 结合在一起感觉没有达到编码的预期效果
whoami9426
2023-12-10 13:57:43 +08:00
@okakuyang 是 Promise 嵌套的问题吗?
whoami9426
2023-12-10 13:59:02 +08:00
附上代码的在线执行地址吧: https://playcode.io/1690189
common0
2023-12-10 14:32:50 +08:00
第二个 then() 里的 Promise.resolve() 前加上 return ,5 、6 就在 3 、4 前面了。
wipbssl
2023-12-10 14:37:58 +08:00
异步逻辑的问题,第二个 then 相当于 async 函数里放了一个 aync 函数,但没有加入 await ,所以执行到 setArr 2 时就异步,主线程继续执行第三个 then 了,5 和 6 要等到 2 的 promise resolve 后才会执行,但任务序列中第 3 个 then 在 setArr 2 resolve 前就加入队列了
chenliangngng
2023-12-10 15:22:06 +08:00
Promise 立即执行,当前轮宏任务
then 当前轮微任务
useState 浏览器空闲,也就是上一轮执行完了,下一轮宏任务
then 下一轮的微任务
onec
2023-12-10 15:36:27 +08:00
第一个 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
2023-12-10 17:55:37 +08:00
简化一下你的代码:

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
2023-12-10 21:48:26 +08:00
你这代码就是改成 console.log 也是 0, 1, 2, 3, 4, 5, 6 吧,和 state 一点关系都没有,纯 Promise 执行顺序问题
8XIQz5SCHX1U6c7s
2023-12-11 10:09:35 +08:00
好好好,又复习了一遍事件循环
lilei2023
2023-12-11 10:12:38 +08:00
这和 setState 没关系吧,这不就是 promise 执行问题么
lilei2023
2023-12-11 10:25:31 +08:00
@lilei2023 不过我也做错了
chanChristin
2023-12-11 16:15:38 +08:00
有意思,所有的 ai 都认为答案是 [0, 1, 2, 5, 6, 3, 4]
CrispyNoodles
2023-12-11 18:00:24 +08:00
好好好,这样玩是吧。本来写完代码只是想摸下鱼,强制让我复习了一遍事件队列,内存,栈,堆的概念

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

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

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

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

© 2021 V2EX