这个问题其实在无关 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 事件或异步事件中是同步的。
如:
在点击按钮后 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。
至此,批量更新原理也就大抵说完了。
如需转载,请注明出处