前言:
本篇去实践验证一些已解释过的理论,用于了解其内部实现,并加深自己对知识点的深刻印象。从一个问题点切入看 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, 随后debugv17.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,从而确定其优先级。
参考链接: