以下代码
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 这个函数是立即执行的。
我真的搞不懂为什么要这么设计?
1
3059349417 308 天前
你没学过编程么,异步不懂?
|
2
MorningStar0 308 天前
调用 prev1 的时候组件更新然后后面的 set 函数都会进入等待更新队列,这是时候执行 console 所以输出了 state 。后面的就进行合并更新了。对于合并更新参考 https://juejin.cn/post/7228851979462737978
|
3
vincenteof 308 天前 1
react.dev 先读一遍
|
4
dcsuibian OP @3059349417 你有看我的内容吗? prev1 这里明显是被同步的调用了(虽然状态没更新),而其他的都是异步的。
|
5
chenliangngng 308 天前
state 1 那是因为这里是个闭包,底层的 A 其实已经改了
|
6
bojackhorseman 308 天前 2
|
7
codehz 308 天前 via iPhone
除了 react 有意将副作用隔离之外还有一个就是语言限制,即你不能修改一个被解构出来的变量(虽然就算你不解构它也不会变)
|
8
dcsuibian OP @chenliangngng 我不是关心这个值,我是关心为什么这个函数是立即执行了
|
9
zackzergzeng 308 天前
要先把 react 这个概念先搞懂(范志毅脸)
|
10
Bijiabo 308 天前
1. 打开 Google
2. 找到输入框,输入“React useState 原理” 3. 按下回车 4. 阅读文章 |
11
angrylid 308 天前 via Android
这个应该是 stale closure
|
13
ivslyyy 308 天前
@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. |
14
wgbx 308 天前
setA 是函数,他映射的 useState 函数是异步函数,console.log('A', 'state', A)展示 1 ,有啥问题?
|
15
clue 308 天前
所以, 你是对时序有要求?
如果是我设计的话, 考虑有个 effect B 依赖 A, 那我修改 A 多次, 这个 effect B 是否要执行多次呢? react 重新执行 hook, 我记得是需要再次执行 render 函数的, 碰到你这个场景, 如果 setState 时正在执行 render, 要怎么处理呢? 总之这种 依赖 -> 响应 的模式很脆弱, 为了防止出问题, 会加很多限制与异步 |
17
dcsuibian OP @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 |
18
Imindzzz 308 天前 via Android
经典面试题“setState 是异步还是同步”
|
19
swaggeek 308 天前 1
@dcsuibian 设值这个过程是一个同步的过程吧,需要执行函数,然后拿到新值,所以立刻执行了,后面的没有立即执行,是因为像 2 楼说的,进入了批量更新的队列,被加了锁了。需要等这次更新完,才能执行,有点像微任务队列吧。
|
20
barbara012 308 天前 6
怎么感觉大多都没 get 到 op 困惑点?
|
21
visper 308 天前 4
可能题主的奇怪点在于,如果那些函数都是放在队列里面等时机再执行,那应该最后的 A state1 就会在 A prev 1 前面。这样卡在中间的话不知道 react 是什么执行规则的。
|
22
JounQin 308 天前
不要学八股文
|
23
codehz 308 天前 via iPhone
要不你试试不同版本的区别(我感觉这应该有差异
|
24
codehz 308 天前 via iPhone
有没有一种可能和 Fiber Reconcilier 的调度有关系
|
25
oatw 308 天前 1
感觉很多兄弟没审题就质疑楼主呢?
没看过这部分源码,盲猜可能是这样: 1. 第一次调用 set 的时候通知 react 要更新了; 2. 在准备 rerender 前,其它 set 操作会被入队,等待一起执行; 3. 执行所有队列中的 set 操作&rerender 。 我觉的关键就是 react 总得知道要更新这件事,而这里就是第一次 set 触发的,里面的函数也真的执行了,然后在真正更新前发现还有其他 set ,所以就入队了其他 set 操作。 |
26
chenliangngng 308 天前
@dcsuibian 在触发更新前,变更函数都会立即执行,因为只有执行了 react 才知道你是否变更,你返回相同值试试。在发生变更之后,剩下的变更函数全部打到更新队列里去
|
27
oatw 308 天前 1
@oatw 补充一下,什么时候执行 set 内函数其实并不是关键,即便执行了,也只是计算了结果暂存下来,并没有真的更新状态变量,状态变量都是在队列中所有函数执行后的最终计算结果下更新。但是私以为第一次 set 的时候内部的函数也可以不立刻执行,而是放到队列中和其他的一起执行,至于为啥,只能读源码了。官方文档也不完全可信。
|
28
johnnyNg 308 天前 1
react 的 hooks 确实很反直觉,我只喜欢 react'的 jsx ,vue'的 watch 、computer 就感觉很符合直觉
|
29
swaggeek 308 天前
@oatw 我看了下 React 文档,他是需要在 render 的时候再去看看队列里有没有 set 函数,然后再执行取值的。感觉这样其实 react 会有很多次无效渲染。像 Vue 就有合并状态的这种操作,最终只会更新一次。
|
30
ljpCN 308 天前
[今天让你彻底搞懂 setState 是同步还是异步]( https://zhuanlan.zhihu.com/p/350332132)
|
33
helIo0o 308 天前 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 那里只在第一次点击的时候,才会同步执行一次,后面都是异步的了 |
35
mikami 308 天前 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)
真想要搞明白为什么这个顺序,我估计只能看源码了。只能说日常工作中不要这么写 |
36
wpzz 308 天前
"
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”. " 如果某些内容在屏幕外,我们可以延迟与其相关的任何逻辑。如果数据到达速度快于帧速率,我们可以合并并批量更新。 |
37
Leviathann 308 天前
react 是协程式的执行方式,不要依赖它调用顺序
|
38
Chingim 308 天前
好问题, 值得写一篇文章
|
39
vincenteof 308 天前
这应该是实现细节,说不定哪个版本以后就变了。updater 如果是 pure function 的话,你可能不需要关心它到底什么时候被执行?
|
40
wpzz 308 天前
@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__ |
41
Pencillll 308 天前 6
https://github.com/reactjs/react.dev/issues/5982#issuecomment-1674183863
> The timing for when the callback is called is not guaranteed. |
42
charlie21 308 天前 via Android
在点击事件之后最新渲染值,Vue 和 React 的这个行为各是出于什么考虑?
https://www.zhihu.com/question/543057656 |
43
realJamespond 308 天前
估计是 setState 第一次直接执行,第二次以后放入队列?这样强行解释满意不😂
|
44
ragnaroks 308 天前 2
react fiber 更新会“自己”选择每批执行的逻辑,同步也好异步也好没法在代码层面保证执行时机,这个设计的目的是确保在大多数情况下不会低于 60 帧,实际上还是看编码人员的水平。
循环 setState 只需要纳秒单位的时间,理论上只会显示最新值,实际上如果机器配置足够低,是能看到中间值的。 |
45
Alander 308 天前 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 里面 |
46
Alander 308 天前
具体代码看 ReactFiberHooks.js 里的 dispatchSetState 方法吧,还是比较清晰明了的,日常还遇到过很多奇葩的问题其实去看一眼源码就大概了解了,react 还是比较线性的
|
47
Mikawa 308 天前
OP 的 React 是哪个版本的
这里 onClick 是个合成事件的回调,可能是受到 Reconcilier 的影响了,用 addEventListener 试试呢 |
48
mxT52CRuqR6o5 308 天前 1
严格模式且开发模式下 setState 后会执行一次 render 检查用户的 useEffect 是否有正确清除副作用
|
50
AQingC 308 天前
楼主 react 版本是多少?
|
51
royzxq 308 天前
这和 fiber 的实现有关.. 具体的话可能要看源码
|
52
rrfeng 308 天前
楼上有人回答了,我半吊子 js/react 水平,但是我知道
setState 和 console.log 都是异步的,所以我不能相信任何它的顺序。 |
53
xuhai951753 308 天前
如果你想问为什么这么设计,就是为了避免大量的渲染,否则每次 set 都会 render 一遍。你可以在 react 非 concurrent 模式下将 onClick 的内容在 setTimeout 中执行做对比。
如果你想问实现,简单来说在合成事件或者 concurrent 模式中第一次 set 之后就会打个标,标记要 update ,后续的所有 set 就会合并到更新时执行,至少是下一个时间切片。 |
54
HTML001 308 天前 1
笑了,前面几楼一上来就嘲讽,但是连问题都没看明白
|
55
zangbianxuegu 308 天前
@Pencillll 大佬是怎么快速搜索/定位到问题解释的呢?
|
56
Chingim 308 天前 2
写了篇文章分析: https://www.overcch.com/posts/useState-callback-timing
TLDR; 因为性能优化, 如果当前 fiber 没有更新, react 会先执行 setState 的回调, 用来做性能优化: 如果 setState 的结果和当前值一致, 就跳过后续的 reconcile 和 render 过程. |