前言
本文仅分析react-dom在开启concurrent mode的下和legacy mode下的setState在setTimeout,Promise等非React合成事件不同的表现的部分源码(即批量更新的部分源码)。 注: 在下文中的源码其实来自react-dom,但在书写的过程中一直写作React。
Legacy Mode 下的批量更新
首先我们通过cra创建一个React项目后使用以下的代码。
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [count, setCount] = useState(0);
console.log('render');
// react合成事件中触发setState
function syncFunc(){
setCount(count + 1);
setCount(count + 1);
}
// 非react合成事件中触发setState
function asyncFunc(){
setTimeout(() => {
setCount(count + 1);
setCount(count + 1);
},0);
}
return (
<>
<span>count: {count}</span>
<br/>
<button onClick={syncFunc}>+1</button>
<br/>
<button onClick={asyncFunc}>setTimeout+1</button>
</>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
当我们点击+1的button时会出现如下的打印
当我们点击
setTimeout+1的button时会出现如下的打印
也就是说当我们在
setTimeout中调用setState时,一共触发了两次App组件的render,而在React的合成事件中只触发了一次App组件的render。
揭秘Legacy Mode下的批量更新
如上文所示,Legacy Mode中两种不同的方式调用的setState的最大的不同点其实是在setTimeout中调用的setState造成了App组件的两次render。那么我们就从App组件进行render时的函数调用栈来查看两者的不同。
以下是React合成事件(即上文的syncFunc函数)调用后的render函数的调用栈
注意: App的render函数是在syncFunc调用结束之后才调用,所以该调用栈中并没有syncFunc的执行上下文。我们可以当作syncFunc函数在batchedUpdates$1函数中进行调用。 以下是React非合成事件(即上文的asyncFunc函数)调用后的render函数的调用栈。
注意: anonymous就是我们在setTimeout中传入的匿名回调函数。dispatchSetState其实就是setCount方法,所以以上的函数调用最终会走两次,所以在setTimeout中最后会打印出两次render。 对比两者之前的差异,我们判断当进入flushSyncCallbacksOnlyInLegacyMode这一函数调用,React进入”render“阶段,即根据当前的需要更新的state的队列来生成新的state并在最后执行App函数后获得新的"App组件"。那么,也就是说在setTimeout函数中的setState会进入到scheduleUpdateOnFiber并且在该方法中最终会调用到flushSyncCallbacksOnlyInLegacyMode从而触发App组件的render。而syncFunc中的setState(dispatchSetState)并没有进入到后续的React的render阶段。 其中scheduleUpdateOnFiber的关键代码如下所示:
// SyncLane为同步更新的标识
// executionContext是否为React合成事件中执行的标识 / 进行批量更新的标识
// fiber.mode & ConcurrentMode) === NoMode 判断当前是否为fiber的模式,
// isBatchingLegacy: react源码中act函数调用后的批量更新标识
if (lane === SyncLane && executionContext === NoContext && (fiber.mode & ConcurrentMode) === NoMode && !( ReactCurrentActQueue$1.isBatchingLegacy)) {
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
那么在知道了executionContext的变量的含义之后,发现executionContext该参数初始化是被赋值为NoContext,后续的逻辑就比较简单了,我们只需要会调用栈中会造成executionContext变化的函数即可,而这个函数就是batchedUpdates$1,该函数源码如下
// fn可以简单理解为我们传递给React中的合成事件,例如上述的syncFunc以及asyncFunc
function batchedUpdates$1(fn, a) {
var prevExecutionContext = executionContext; // 保存原标识
executionContext |= BatchedContext; // 将标识更新为批量更新标识
try {
return fn(a);
} finally {
// 在React合成事件执行之后
executionContext = prevExecutionContext; //恢复原标识 -> NoContext
// 在Legacy mode下 进入React的”render“逻辑
if (executionContext === NoContext && !( ReactCurrentActQueue$1.isBatchingLegacy)) {
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}
}
根据以上的源码和注释我们就可以发现React中setState在两个函数中展示不同的原因其实是在batchedUpdates$1函数中会在执行fn函数之前将executionContext变量设置为BatchedContext,在fn函数执行之后再将executionContext修改回原来的值,并且执行flushSyncCallbacksOnlyInLegacyMode进行render。
Concurrent mode中的批量更新
我们使用以下的代码来开启React的Concurrent mode
import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';
const App = () => {
const [count, setCount] = useState(0);
console.log('render')
function syncFunc(){
setCount(count + 1)
setCount(count + 1)
}
function asyncFunc(){
setTimeout(() => {
setCount(count + 1)
setCount(count + 1)
}, 0);
}
return (
<div>
<span>{count}</span>
<br />
<button onClick={syncFunc}>+1</button>
<br />
<button onClick={asyncFunc}>setTimeout+1</button>
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<App />
);
在开启Concurrent mode下无论是syncFunc函数还是asyncFunc函数最终都会出现如下的打印。
揭秘Concurrent mode的批量更新
以下是syncFunc调用后触发render的函数调用栈
其中是由一个匿名函数的调用flushSyncCallbacks来触发React中的后续的render逻辑。那么我们看看调用这个匿名函数的ensureRootIsScheduled的函数逻辑,以下为ensureRootIsScheduled的部分源码以及注释。
// root -> 当前的FiberRootNode
function ensureRootIsScheduled(root, currentTime) {
var existingCallbackPriority = root.callbackPriority;
// 当existingCallbackPriority为newCallbackPriority,不再调用flushSyncCallbacks
if (existingCallbackPriority === newCallbackPriority){
return;
}
// 是否为同步更新
if (newCallbackPriority === SyncLane) {
// FirbeRootNode中的tag是否为LegacyRoot,如果是则执行performSyncWorkOnRoot
if (root.tag === LegacyRoot) {
// 将当前的react-dom设置为触发渲染的状态 - 最终也会调用scheduleSyncCallback
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 将当前的更新渲染函数加入到syncQueue队列中,在flushSyncCallbacks
// 会进行出队并进行更新
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
// 用Promise实现微任务 / setTimeout模拟微任务
scheduleMicrotask(function () {
// 当前不在React的合成事件中
if (executionContext === NoContext) {
flushSyncCallbacks();
}
});
}
}else{
//不是同步更新
...
}
// 设置root.callbackPriority为 newCallbackPriority
root.callbackPriority = newCallbackPriority
...
}
以下是syncFunc调用后触发render的函数调用栈
注意: anonymous就是我们传给setTimeout的回调函数,所以与React的合成事件相比的差异是在ensureRootIsScheduled中调用scheduleCallback$2。那么我们就继续分析ensureRootIsScheduled的部分源码.
// root -> 当前的FiberRootNode
function ensureRootIsScheduled(root, currentTime) {
// 个人认为: 获得当前render的优先级 -> setTimeout中返回16
var newCallbackPriority = getHighestPriorityLane(nextLanes);
var existingCallbackPriority = root.callbackPriority;
// 当existingCallbackPriority为newCallbackPriority,不再调用flushSyncCallbacks
if (existingCallbackPriority === newCallbackPriority){
return;
}
// 是否为同步更新
if (newCallbackPriority === SyncLane) {
...
else{
//不是同步更新
// 个人认为schedulerPriorityLevel为当前的任务等级
newCallbackNode = scheduleCallback$2(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
而这里的nextLane与React的currentUpdatePriority参数息息相关,有关syncFunc和asyncFunc中的对nextLane出现的差异也来自这个参数。有关修改这个参数的函数调用是dispatchDiscreteEvent函数,dispatchDiscreteEvent部分源码如下.
function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
// 保存上一次的优先级 -> 默认为NoLane = 0
var previousPriority = getCurrentUpdatePriority();
try {
// 修改currentUpdatePriority为SyncLane
setCurrentUpdatePriority(DiscreteEventPriority);
// 以下为触发React的合成事件即上述的syncFunc以及asyncFunc
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
} finally {
// 修改回上一次的优先级
setCurrentUpdatePriority(previousPriority);
}
}
dispatchDiscreteEvent的方法也是在调用React合成事件时先保存currentUpdatePriority后,将currentUpdatePriority为SyncLane再调用React的合成事件,从而保证React合成事件中的setState的优先级。而在调用结束后则替换为原先的currentUpdatePriority。 而scheduleCallback$2方法中做了什么呢,其实是React中的调度算法,他会根据任务的优先级而来调用“render”任务。最终React是使用MessageChannel中调用performWorkUntilDeadline。
结束语
以上就是有关react-dom的concurrent mode和legacy mode两种模式下的批量更新的部分源码阅读,如果对以上的源码阅读有异议,欢迎纠正。