前言:
本篇去实践验证一些已解释过的理论,用于了解其内部实现,并加深自己对知识点的深刻印象。从一个问题点切入看 React Hook,以此记录深入其过程。
setState 拿不到最新值?
还是那个莫名深夜,朋友突然甩给我一个 Demo 问我为什么setState 改变了下一步拿不到最新值?
Demo: codesandbox.io/s/getnewsta…
const Demo = () => {
// useState update lane
const [num, setNum] = useState(1);
const [str, changeStr] = useState("现在是数字1");
const changeNum = () => {
setNum(num + 1);
const newStr = "现在数字是" + num;
console.log(newStr); // 现在数字是1
changeStr(newStr);
};
return (
<div>
<button onClick={changeNum}>click +1</button>
<div>{num}</div>
<div>{str}</div>
</div>
)
}
于是给他解释道:
组件内部的任何函数,包括事件处理函数和 effect,都是从它被创建的那次渲染中被「看到」的。
其实React Function
组件可以被看做是一个闭包,里面的Function
使用了组件的变量,此时是当前渲染的num
即 1 ,而setNum
改变state
需要在下一次更新之后被渲染出来。这也就是大家所说的 setState
是异步的。那如何理解他们说的这种异步呢?这涉及到setState
之后React
的流程。
diapatch
=> ...... => scheduleUpdateOnFiber
( reconcile ) => ensureRootIsScheduled
(scheduled) => commit
其中ensureRootIsScheduled
步骤涉及到了是否会批量更新的判断(后面会解答),这几个步骤的主要行为是会把执行的task
放入workLoop
中,所以并不是一个同步执行的任务,在这一系列方法执行之后会触发组件 rerender
,在这之前组件的num
不会变,也就是此时执行changeStr
之后,不能实时拿到最新的num
的原因。
setState 如何能拿到最新值?
朋友继续说,changeNum
拿不到最新的 num
,所以他一般会使用 useEffect
依赖 num
去获取,总感觉哪里有点别扭,有没有好点的方法,不想写太多的 useEffect
?
于是给了他提供了以下方法:
setFoo(newState => {
console.log('newState', newState)
return newState
});
setState
可以传一个Function
,而此时拿到的就是newState
。为什么这样就能拿到newState
?
结论:因为setFoo
传入的是一个Function
,最终步骤二中整合最新state
时,会判断传进来的action
是否为Function
,此时会取到当前最新的state
做操作,所以一直能拿到最新的。
setTimeout 中为什么 setState 变成异步操作了?
过了几天朋友又问了个问题:为什么在 setTimeout
中执行了多次 setState
之后,不是同步变成异步操作了?顺便甩了几个 Demo
给我。
v17: codesandbox.io/s/react-17-…
v18:codesandbox.io/s/usestate-…
我告诉他React 18
已经将setTimeout
中的setState
也加入了批量更新的逻辑(Automatic batching)1,后续可能就没有setState
同步概念了。
看了些文章发现React 18
是以优先级为依据来做自动批处理(Automatic batching)2, 随后debug
v17.0.0 和 v18.0.0 源码,找到两个关键函数scheduleUpdateOnFiber
、ensureRootIsScheduled
,发现其中有段逻辑做了调整:
scheduleUpdateOnFiber
:
v17:
v18:
可以看出 v18.0.0 把lane === syncLane
逻辑调整到内部,而该逻辑的代码就是触发立即执行commit
的代码,v17.0.0 的代码也与lane
有关,于是便列出了与lane
的信息:
版本 | lane | executionContext |
---|---|---|
v17.0.2 | 1 === syncLane | 0 |
v18.0.0 | 16 (基于事件的类型像宿主环境(window)寻求优先级) | 0 |
ensureRootIsScheduled
:
v17:
v18:
v18 在设置了lane = 16
之后,使其不会再进入commit
阶段,从而在ensureRootIsScheduled
中能进行batch update
判断。而 v17 未针对setTimeout
中的代码特别去设置lane
,使其每次都能进入commit
流程,所以在ensureRootIsScheduled
始终无法进行batch update
判断(因为每次都直接执行了commit
,也就无须做batch update
)
问答:
- 是否只要是执行 setState 都会执行
batch update
,无论是否是同一个 useState?以优先级的逻辑执行批处理的话,同一优先级的都应该在一个batch update
中?
不同setState
同样会执行同一个batch update
,且同一优先级都在一个batch update
中。
onClick
时,executionContext
=== 1?
executionContext
=== BatchedContext
=== 1
即使onClick
的lane === syncLane
,但是由于onClick
的executionContext
会被赋予BatchedContext
所以始终会走batch update
。
- 18 版本如何让 React 能识别
setTimeout
等事件使其能被设置优先级(lane
)
此处获取window.event
以获取具体的event lane
。(基于事件的类型像宿主环境(window)寻求优先级)
总结:
以优先级为主来实现 Automatic Batching,之前也是这样做的,但是只能实现半自动化,即正常触发setState
会走batch update
,而setTimeout
/setInterval
/Promise.then(fn)
/fetch
回调/xhr
网络回调时,React 都是无法控制的,所以并未实现batch update
。所以为了做到全自动化,在执行这些非事务操作时,React 会去请求event lane
,从而确定其优先级。
参考链接: