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

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

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

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
150 天前
你这代码就是改成 console.log 也是 0, 1, 2, 3, 4, 5, 6 吧,和 state 一点关系都没有,纯 Promise 执行顺序问题
ifOnly
149 天前
好好好,又复习了一遍事件循环
lilei2023
149 天前
这和 setState 没关系吧,这不就是 promise 执行问题么
lilei2023
149 天前
@lilei2023 不过我也做错了
chanChristin
149 天前
有意思,所有的 ai 都认为答案是 [0, 1, 2, 5, 6, 3, 4]
CrispyNoodles
149 天前
好好好,这样玩是吧。本来写完代码只是想摸下鱼,强制让我复习了一遍事件队列,内存,栈,堆的概念

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

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

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

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

© 2021 V2EX