react 的setState是同步还是异步的()

这个问题其实在有关react技术栈的面试中经常会遇到,这次在这里记录这个问题是由于最近的一次面试,面试官问了这个问题,我跟面试官各执己见,我的回答是在react合成事件中是异步,在原生事件或者异步事件中是同步;而后遭到否定,面试官认为,setState就是异步的,为此还争吵了一番,最终在面试官认为我太过钻牛角尖而结束。所以这个问题我是特别印象深刻的,后面我也翻看过部分源码,就来说说我在其中的理解。
版本与技术栈:
react : 16.8.1
react-dom: 16.8.1
umd方式引入:
react: https://cdn.bootcdn.net/ajax/...
react-dom: https://cdn.bootcdn.net/ajax/...
先放结论:react 的setState 在react合成事件中是异步的,在非react合成事件,如原生js事件或异步事件中是同步的。
如:
react 的setState是同步还是异步的()
文章图片

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

我们在componentDidMount生命周期中挂载我们的原生click事件于document中,在页面空白处点击,会打印出render、123、render、321,也就是每次的setState都会引发一次render。
上面的两个例子也刚好对应上了我们一开始的结论,那为什么会出现这种情况?跟react是怎么处理的呢?这个让我们到源码中去找寻~
从事件看源码:
如何从事件中看源码呢?
在我们刚刚讲到的第一个例子中,我们通过一个button点击触发更新事件,那么我们可以在这个事件中下手,找到我们的突破点。
第一步:我们查看button元素的事件绑定
react 的setState是同步还是异步的()
文章图片

在控制台事件监听器中,我们发现该button有着两个click事件绑定,其中一个是document的click事件,一个button的click事件,我们点击button事件进入看看:
react 的setState是同步还是异步的()
文章图片

我们发现在该事件中只有个noop方法,且在里面没有任何操作,what?
react 的setState是同步还是异步的()
文章图片

就这样没了嘛?当然不是!别忘了在之前的事件监听器中有着两个事件绑定,虽然后面那个没有直接绑定到button元素上,但在事件冒泡中,我们点击了button后会往上冒泡到document元素上,而在document元素上绑定的事件才是我们实际会进行操作的方法。
所以我们再来看看事件监听器中的另外那个事件做了什么呢?
我们可以在第一个例子中(有批量更新的例子)中把该监听方法移除,而后点击我们的button元素执行更新,我们会发现这时候本该打印出 123、321、render 变成了 render、123、render、321,也就是没有了批量更新操作了,那么我们可以确定批量更新功能便是在这个方法中处理的。
第二步: 从我们找到的方法入口查看源码执行
我们点击找到的方法,看看里面执行的是哪个方法:
react 的setState是同步还是异步的()
文章图片

我们发现里面执行的是dispatchInteractiveEvent这个方法,我们去到该方法中debugger下查看执行的顺序:
react 的setState是同步还是异步的()
文章图片

我们在debugger中发现下一个执行方法是 function interactiveUpdates$1:
react 的setState是同步还是异步的()
文章图片

可以发现,在该方法中有个isBatchingUpdates变量,这个isBatchingUpdates便是处理批量更新重要的一点,isBatchingUpdates会在setState方法执行过程中用于判断是否批量更新。
我们接着往下看,在debugger中,我们走到了fn方法中,fn方法是我们在dispatchInteractiveEvent方法中传入的dispatchEvent方法;
所以我们再来看看dispatchEvent方法:
react 的setState是同步还是异步的()
文章图片

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

//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方法中:
react 的setState是同步还是异步的()
文章图片

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
react 的setState是同步还是异步的()
文章图片

执行往下后找到classComponentUpdater对象中的enqueueSetState方法,再往下找到scheduleWork 方法后找到requestWork方法:
react 的setState是同步还是异步的()
文章图片

在该方法中,我们可以看到根据isBatchingUpdates判断,当isBatchingUpdates为true时return,不再往下执行render,这就是批量更新等待所有setState执行完后再更新的全部。
补充:
react的setState在react的合成事件中为异步执行,这个异步执行实际上依旧是同步执行,只是等待所有setState执行完成后再进行render。
至此,批量更新原理也就大致说完了。
【react 的setState是同步还是异步的()】如需转载,请注明出处

    推荐阅读