useState 获取最新值到 Automatic-batching 的延伸思考

459 阅读4分钟

前言:

       本篇去实践验证一些已解释过的理论,用于了解其内部实现,并加深自己对知识点的深刻印象。从一个问题点切入看 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 源码,找到两个关键函数scheduleUpdateOnFiberensureRootIsScheduled,发现其中有段逻辑做了调整:

scheduleUpdateOnFiber

v17:

v18:

可以看出 v18.0.0 把lane === syncLane逻辑调整到内部,而该逻辑的代码就是触发立即执行commit的代码,v17.0.0 的代码也与lane有关,于是便列出了与lane的信息:

版本laneexecutionContext
v17.0.21 === syncLane0
v18.0.016 (基于事件的类型像宿主环境(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

即使onClicklane === syncLane ,但是由于onClickexecutionContext会被赋予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,从而确定其优先级。

参考链接:

  1. github.com/reactwg/rea…
  2. zhuanlan.zhihu.com/p/382216973