react 的setState是同步还是异步的?

发布于2023-04-15 16:39:12字数46554

前言

这个问题其实在有关 react 技术栈的面试中经常会遇到,这次在这里记录这个问题是由于最近的一次面试,面试官问了这个问题,我的回答是在 react 合成事件中是异步,在原生事件或者异步事件中是同步;而后遭到否定,面试官认为,setState 就是异步的,为此还争吵了一番,最终在面试官认为我太过钻牛角尖而结束。所以这个问题我是特别印象深刻的,后面我也翻看过部分源码,就来说说我在其中的理解。

版本与技术栈:

先放结论:react 的 setState
在 react 合成事件中是异步的,在非 react 合成事件,如原生 js 事件或异步事件中是同步的。

如:

在点击按钮后 render 方法中只打印了一次 render,打印顺序分别为
123,321,render
这是由于 react 在使用 setState 去操作修改我们的 state 时,是在我们的 react
jsx 中绑定的点击事件,而该事件为 react 合成事件,会有批量更新操作,这也就是异步操作。

我们再来一个无批量更新,也就是同步操作:

我们在 componentDidMount 生命周期中挂载我们的原生 click 事件于 document 中,在页面空白处点击,会打印出render、123、render、321,也就是每次的 setState 都会引发一次 render。

上面的两个例子也刚好对应上了我们一开始的结论,那为什么会出现这种情况?跟 react 是怎么处理的呢?这个让我们到源码中去找寻~

从事件看源码:

如何从事件中看源码呢?

在我们刚刚讲到的第一个例子中,我们通过一个 button 点击触发更新事件,那么我们可以在这个事件中下手,找到我们的突破点。

第一步:我们查看 button 元素的事件绑定

在控制台事件监听器中,我们发现该 button 有着两个 click 事件绑定,其中一个是 document 的 click 事件,一个 button 的 click 事件,我们点击 button 事件进入看看:

我们发现在该事件中只有个 noop 方法,且在里面没有任何操作,what?

就这样没了嘛?当然不是!别忘了在之前的事件监听器中有着两个事件绑定,虽然后面那个没有直接绑定到 button 元素上,但在事件冒泡中,我们点击了 button 后会往上冒泡到 document 元素上,而在 document 元素上绑定的事件才是我们实际会进行操作的方法。

所以我们再来看看事件监听器中的另外那个事件做了什么呢?

我们可以在第一个例子中(有批量更新的例子)中把该监听方法移除,而后点击我们的 button 元素执行更新,我们会发现这时候本该打印出
123、321、render变成了
render、123、render、321,也就是没有了批量更新操作了,那么我们可以确定批量更新功能便是在这个方法中处理的。

第二步: 从我们找到的方法入口查看源码执行

我们点击找到的方法,看看里面执行的是哪个方法:


我们发现里面执行的是 dispatchInteractiveEvent 这个方法,我们去到该方法中 debugger 下查看执行的顺序:

我们在 debugger 中发现下一个执行方法是 function interactiveUpdates$1:

可以发现,在该方法中有个 isBatchingUpdates 变量,这个isBatchingUpdates便是处理批量更新重要的一点,isBatchingUpdates会在setState方法执行过程中用于判断是否批量更新。

我们接着往下看,在 debugger 中,我们走到了fn方法中,fn 方法是我们在]dispatchInteractiveEvent方法中传入的dispatchEvent方法;

所以我们再来看看dispatchEvent方法:

先来解释下具体做了什么:

//topLevelType是事件类型,在这次demo中是click
//nativeEvent是一个基于原生Event的对象,描述已发生的事件(既原生addEventListener事件回调函数参数)

function dispatchEvent(topLevelType, nativeEvent) {
   if (!_enabled) {
       return;
   }
   //获取当前事件target节点
   var nativeEventTarget = getEventTarget(nativeEvent);

   //获取target节点addEventListener处理事件,如我们button点击中添加的click事件
   var targetInst = getClosestInstanceFromNode(nativeEventTarget);
   if (targetInst !== null && typeof targetInst.tag === "number" && !isFiberMounted(targetInst)) {
       targetInst = null;
   }

   var bookKeeping = getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst);

   try {
       batchedUpdates(handleTopLevel, bookKeeping);
   } finally {
       releaseTopLevelCallbackBookKeeping(bookKeeping);
   }
}

方法执行到这里,我们已经知道 react 是如何通过全局的事件代理获取我们操作的事件类型与处理方法,我们接着 debugger 往下看,中间省略 react 处理,我们会发现又回到了我们的interactiveUpdates$1方法中:


dispatchEvent方法执行完成,且在打印台中我们发现已经打印了123、321
这就说明我们定义的方法已经被执行了,而后在interactiveUpdates$1方法中将isBatchingUpdates设置为 false,并往下执行 render。

方法执行到这里,我们应该已经大概明白了 react 批量更新是怎么回事了,在开始的interactiveUpdates$1方法中通过设置isBatchingUpdates为 true,然后通过全局的事件代理获取我们所操作的事件类型与处理方法,而后在interactiveUpdates$1执行上下文中执行我们定义的 handle 方法,如果在定义的 handle 方法中执行 setState,那么其执行上下文中的isBatchingUpdates为 true,执行批量更新操作。

那么在原生 js 事件中无法批量更新便就可以解释了,因为我们的批量更新是由全局事件代理获取 react 合成事件操作方法的,并不绑定原有操作元素节点,通过绑定 document 事件监听事件代理处理,这就有了可控的执行上下文去指定批量更新,而原生 js 事件的执行并不通过该 document 节点上的代理,这便无法执行批量更新;而至于为什么在 react 合成事件中的异步函数中也没有批量更新,那则是由于异步函数的执行上下文已经脱离了我们的全局事件代理上下文,这也就是批量更新原理。

顺便看看 setState 是如何根据 isBatchingUpdates 控制批量更新的:

在我们第一个例子上 debugger

执行往下后找到classComponentUpdater对象中的enqueueSetState方法,再往下找到scheduleWork
方法后找到requestWork方法:

在该方法中,我们可以看到根据isBatchingUpdates判断,当isBatchingUpdates为 true 时 return,不再往下执行 render,这就是批量更新等待所有setState执行完后再更新的全部。

补充:

react 的 setState 在 react 的合成事件中为异步执行,这个异步执行实际上依旧是同步执行,只是等待所有 setState 执行完成后再进行 render。

至此,批量更新原理也就大致说完了。


如需转载,请注明出处

评论

jane

nishizhu

  • 回复
back top