原文链接
首先我们先大体看一下状态更新的整个调用路径的关键节点:
状态更新流程开始后首先会创建 Update 对象,所以首先我们需要了解 Update 的结构与工作流程。
首先,触发更新的方式主要有以下几种:
由于不同类型组件工作方式不同,所以存在两种不同结构的 Update,其中 ClassComponent 与 HostRoot 共用一套Update 结构,FunctionComponent 单独使用一种 Update 结构;虽然他们的结构不同,但是他们工作机制与工作流程大体相同。有关 hooks 的内容,会专门写一篇 hooks 相关的代码解析。
ClassComponent 与 HostRoot(即 rootFiber.tag 对应类型)共用同一种 Update 结构。HostRoot或者ClassComponent 触发更新后,会在函数 createUpdate 中创建 update,并在后面的 render 阶段的 beginWork 中计算 Update。
Update 对应的结构如下:
const update: Update<*> = {
eventTime,
lane,
tag: UpdateState,
payload: null,
callback: null,
next: null,
};
我们主要关注这些参数:
注意这里的 lane 属性是表示当前更新的优先级,react 中使用 Lane 多车道优先级模型,后面会写一篇有关 Lane 模型相关的文章。
类似 Fiber 节点组成 Fiber 树,Fiber 节点上的多个 Update 会组成链表并被包含在 fiber.updateQueue 中。
ClassComponent 与 HostRoot 使用的 UpdateQueue 结构如下:
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
interleaved: null,
lanes: NoLanes
},
effects: null,
};
字段说明如下:
对于 HostRoot 或者 ClassComponent 会在 mount 的时候使用 initializeUpdateQueue 创建 updateQueue,然后将 updateQueue 挂载到 fiber 节点上:
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
interleaved: null,
lanes: NoLanes
},
effects: null,
};
fiber.updateQueue = queue;
}
enqueueUpdate 用来将 update 加入 updateQueue 队列:
export function enqueueUpdate<State>(
fiber: Fiber, // fiberRoot
update: Update<State>,
lane: Lane,
) {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return;
}
// 当前队列和缓存队列共享一个持久化队列
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
// 比较 fiber lane 和 lane,相同时更新
// render 初始化时不执行
if (isInterleavedUpdate(fiber, lane)) {
const interleaved = sharedQueue.interleaved;// 交错更新
if (interleaved === null) {
// 如果是第一次更新,创建双向链表
update.next = update;
// 在当前渲染结束时,将显示此队列的交错更新
// 被转移到挂起队列。
pushInterleavedQueue(sharedQueue);
} else {
// interleaved.next -> update.next update - interleaved.next;
// interleaved.next = update
// update.next = interleaved.next = update
update.next = interleaved.next;
interleaved.next = update;
}
sharedQueue.interleaved = update;
} else {
const pending = sharedQueue.pending;
if (pending === null) {
// 这是第一次更新。创建单向链表
update.next = update;
} else {
// 定义双向列表
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
}
InterleavedUpdate 是进行交错更新的处理,在渲染过程中途出现的 update,被称为交错更新,在更新队列中,有两个单链表队列字段:pending 和 interleaved 。在我们调度一个交错更新 update 时,它会被储存在 interleaved 属性中。然后整个字段都会被推送到一个全局数组变量上。在当前渲染结束之后,遍历全局数组变量,将交错更新转移到 pending 队列中。
状态更新由用户交互产生,用户心里对交互执行顺序有个预期。React 根据人机交互研究的结果中用户对交互的预期顺序为交互产生的状态更新赋予不同优先级。
具体如下:
React 中有三套优先级机制:
// 离散事件优先级,例如:点击事件,input 输入等触发的更新任务,优先级最高
export const DiscreteEventPriority: EventPriority = SyncLane;
// 连续事件优先级,例如:滚动事件,拖动事件等,连续触发的事件
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
// 默认事件优先级,例如:setTimeout 触发的更新任务
export const DefaultEventPriority: EventPriority = DefaultLane;
// 闲置事件优先级,优先级最低
export const IdleEventPriority: EventPriority = IdleLane;
可以看到 React 的事件优先级的值还是使用的 Lane 的值,这里不直接使用 Lane 也是为了不与 Lane 机制耦合;Scheduler 是一个单独的包,可以用在其他需要任务调度的场景中。
export function lanesToEventPriority(lanes: Lanes): EventPriority {
// 找到优先级最高的lane
const lane = getHighestPriorityLane(lanes);
if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
return DiscreteEventPriority;
}
if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
return ContinuousEventPriority;
}
if (includesNonIdleWork(lane)) {
return DefaultEventPriority;
}
return IdleEventPriority;
}
export const NoPriority = 0; // 没有优先级
export const ImmediatePriority = 1; // 立即执行任务的优先级,级别最高
export const UserBlockingPriority = 2; // 用户阻塞的优先级
export const NormalPriority = 3; // 正常优先级
export const LowPriority = 4; // 较低的优先级
export const IdlePriority = 5; // 优先级最低,闲表示任务可以闲置
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
...
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
}
lanesToEventPriority 函数就是上面 Lane 优先级转换为 React 事件优先级的函数,先将 lane 的优先级转换为 React事件的优先级,然后再根据 React 事件的优先级转换为 Scheduler 的优先级。
// lane使用 31 位二进制来表示优先级车道共31条, 位数越小(1的位置越靠右)表示优先级越高
export const TotalLanes = 31;
// 没有优先级
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
// 同步优先级,表示同步的任务一次只能执行一个,例如:用户的交互事件产生的更新任务
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
// 连续触发优先级,例如:滚动事件,拖动事件等
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
// 默认优先级,例如使用setTimeout,请求数据返回等造成的更新
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultLane: Lanes = /* */ 0b0000000000000000000000000010000;
// 过度优先级,例如: Suspense、useTransition、useDeferredValue等拥有的优先级
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111111000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane4: Lane = /* */ 0b0000000000000000000001000000000;
const TransitionLane5: Lane = /* */ 0b0000000000000000000010000000000;
const TransitionLane6: Lane = /* */ 0b0000000000000000000100000000000;
const TransitionLane7: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLane8: Lane = /* */ 0b0000000000000000010000000000000;
const TransitionLane9: Lane = /* */ 0b0000000000000000100000000000000;
const TransitionLane10: Lane = /* */ 0b0000000000000001000000000000000;
const TransitionLane11: Lane = /* */ 0b0000000000000010000000000000000;
const TransitionLane12: Lane = /* */ 0b0000000000000100000000000000000;
const TransitionLane13: Lane = /* */ 0b0000000000001000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000000010000000000000000000;
const TransitionLane15: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane16: Lane = /* */ 0b0000000001000000000000000000000;
const RetryLanes: Lanes = /* */ 0b0000111110000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;
const RetryLane5: Lane = /* */ 0b0000100000000000000000000000000;
export const SomeRetryLane: Lane = RetryLane1;
export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
可以看到 lane 使用 31 位二进制来表示优先级车道,共 31 条, 位数越小(1的位置越靠右)表示优先级越高,某些相邻的赛道拥有相同优先级。
其中,同步优先级占用的赛道为第一位:
export const SyncLane: Lane = /*
从 SyncLane 往下一直到 SelectiveHydrationLane,赛道的优先级逐步降低。
接下来我们根据 ReactDOM.render、this.setState 这两种触发更新的方式来进行分析状态更新的流程;
ReactDOM.render 作为 react 应用程序的入口函数,进行页面 dom 的首次创建;首次执行 ReactDOM.render 会创建 fiberRootNode 和 rootFiber。其中 fiberRootNode 是整个应用的根节点,rootFiber 是要渲染组件所在组件树的根节点。
window.addEventListener('message', function onMessage {
})
监听到 message,然后执行在 requestHostCallback 中注册的回调函数 flushWork;
至此整个调度结束,接下来对每个步骤代码进行解析;
首先调用 createRoot 方法创建根节点(FiberRootNode)后,会为 root 这个节点做事件委托:
export function createRoot(
container: Container,
options?: CreateRootOptions,
): RootType {
...
// 创建容器 Fiber 根节点
const root = createContainer(
container,
ConcurrentRoot,
hydrate,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
// 在 root 容器上添加事件监听,做事件委托
listenToAllSupportedEvents(rootContainerElement);
}
在 listenToAllSupportedEvents 中会给已注册的事件进行循环遍历添加事件监听,会调用 addTrappedEventListener 方法,在 addTrappedEventListener 方法中会调用 createEventListenerWrapperWithPriority 为这些事件赋予不同的优先级:
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
// 根据不同的事件做优先级分类
const eventPriority = getEventPriority(domEventName);
// 根据优先级分类,设置事件触发时的优先级
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
我们看到首先会调用 getEventPriority 方法,这个方法内部主要是将不同的事件区分为不同的优先级:
export function getEventPriority(domEventName: DOMEventName): * {
switch (domEventName) {
case 'cancel':
case 'click':
case 'copy':
case 'dragend':
case 'dragstart':
case 'drop':
...
case 'focusin':
case 'focusout':
case 'input':
case 'change':
case 'textInput':
case 'blur':
case 'focus':
case 'select':
// 同步优先级
return DiscreteEventPriority;
case 'drag':
case 'mousemove':
case 'mouseout':
case 'mouseover':
case 'scroll':
...
case 'touchmove':
case 'wheel':
case 'mouseenter':
case 'mouseleave':
// 连续触发优先级
return ContinuousEventPriority;
...
default:
return DefaultEventPriority;
}
}
从这个方法中可以很清晰的看到,React 将用户点击,input 框输入等都设置为同步优先级,这是因为用户在操作的时候需要立即得到反馈,如果操作完没有反馈就会给用户造成界面卡顿的感觉。
接下来会根据获取到的事件的优先级分类,设置事件触发时拥有相对应优先级的回调函数:
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
function dispatchDiscreteEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
...
setCurrentUpdatePriority(DiscreteEventPriority);
}
function dispatchContinuousEvent(
domEventName,
eventSystemFlags,
container,
nativeEvent,
) {
...
setCurrentUpdatePriority(ContinuousEventPriority);
}
可以看到相对应回调函数中都调用了同一个方法 setCurrentUpdatePriority,并且都设置了当前事件相对应的事件优先级的值。
在 updateContainer 中首先调用 requestUpdateLane 来获取到当前事件对应的 Lane 优先级;
export function requestUpdateLane(fiber: Fiber): Lane {
// 获取到当前渲染的模式:sync mode(同步模式) 或 concurrent mode(并发模式)
const mode = fiber.mode;
if ((mode & ConcurrentMode) === NoMode) {
// 检查当前渲染模式是不是并发模式,等于 NoMode 表示不是,则使用同步模式渲染
return (SyncLane: Lane);
} else if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
// workInProgressRootRenderLanes 是在任务执行阶段赋予的需要更新的 fiber 节点上的 lane 的值
// 当新的更新任务产生时,workInProgressRootRenderLanes 不为空,则表示有任务正在执行
// 那么则直接返回这个正在执行的任务的 lane,那么当前新的任务则会和现有的任务进行一次批量更新
return pickArbitraryLane(workInProgressRootRenderLanes);
}
// 检查当前事件是否是过渡优先级
// 如果是的话,则返回一个过渡优先级
// 过渡优先级的分配规则:
// 产生的任务 A 给它分配为 TransitionLanes 的第一位:TransitionLane1 = 0b0000000000000000000000001000000
// 现在又产生了任务 B,那么则从A的位置向左移动一位: TransitionLane2 = 0b0000000000000000000000010000000
// 后续产生的任务则会一次向后移动,直到移动到最后一位
// 过渡优先级共有 16 位: TransitionLanes = 0b0000000001111111111111111000000
// 当所有位都使用完后,则又从第一位开始赋予事件过渡优先级
const isTransition = requestCurrentTransition() !== NoTransition;
if (isTransition) {
if (currentEventTransitionLane === NoLane) {
currentEventTransitionLane = claimNextTransitionLane();
}
return currentEventTransitionLane;
}
// 在react的内部事件中触发的更新事件,比如:onClick等,会在触发事件的时候为当前事件设置一个优先级,可以直接拿来使用
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
return updateLane;
}
// 在react的外部事件中触发的更新事件,比如:setTimeout等,会在触发事件的时候为当前事件设置一个优先级,可以直接拿来使用
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;
}
它本质就做了一件简单的事:把当前创建的更新对象 Update 存入当前 Fiber 实例的 updateQueue.shared 队列中,updateQueue.shared 以链表结构存在,通过 next 指针链接下一个 Update 对象;共享队列 sharedQueue 可以和 workInProgress 和 FiberRoot 进行共享队列。
export function enqueueUpdate<State>(
fiber: Fiber,//fiberRoot
update: Update<State>,
lane: Lane,
) {
const updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return;
}
// 当前队列和缓存队列共享一个持久化队列
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;
// 比较fiber lane和lane,相同时更新
// render初始化时不执行
if (isInterleavedUpdate(fiber, lane)) {
const interleaved = sharedQueue.interleaved;//交错更新
if (interleaved === null) {
// 如果是第一次更新,创建双向链表
update.next = update;
//在当前渲染结束时,将显示此队列的交错更新
//被转移到挂起队列。
pushInterleavedQueue(sharedQueue);
} else {
// interleaved.next -> update.next update - interleaved.next;
// interleaved.next = update
// update.next = interleaved.next = update
update.next = interleaved.next;
interleaved.next = update;
}
sharedQueue.interleaved = update;
} else {
const pending = sharedQueue.pending;
if (pending === null) {
//这是第一次更新。创建单向链表。
update.next = update;
} else {
// 定义双向列表
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
}
参数:
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// 检查有没有嵌套更新,上限为 50 次
checkForNestedUpdates();
// 收集需要更新的子节点的 lane,存放在父 fiber 上的 childLanes 上
// 更新当前 fiber 节点的 lannes,表示当前节点需要更新
// 从当前需要更新的 fiber 节点向上遍历,遍历到根节点(root fiber),
// 并更新每个 fiber 节点上的 childLanes属性
// childLanes有值表示当前节点下有子节点需要更新;
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
}
// 将当前需要更新的 lane 添加到 fiber root 的 pendingLanes 属性上,
// 表示有新的更新任务需要被执行
// 通过计算出当前 lane 的位置,并添加事件触发时间到 eventTimes 中
markRootUpdated(root, lane, eventTime);
if (
(executionContext & RenderContext) !== NoLanes &&
// 如果是正在进行中(更新操作中)的 root
root === workInProgressRoot
) {
// 判断是否在 render 阶段进行更新
warnAboutRenderPhaseUpdatesInDEV(fiber);
workInProgressRootRenderPhaseUpdatedLanes = mergeLanes(
workInProgressRootRenderPhaseUpdatedLanes,
lane,
);
} else {
// 判断是否有在跟踪
if (enableUpdaterTracking) {
// 是否是开发环境
if (isDevToolsPresent) {
// 添加 Fiber 的 Map 关系
addFiberToLanesMap(root, fiber, lane);
}
}
if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
...
}
// 如果是正在进行中(更新操作中)的 root
if (root === workInProgressRoot) {
// 如果 root 是正在更新的树,如果 root 不是
// deferRenderPhaseUpdateToNextBatch(已关闭,处于渲染阶段)标志,
// 在该树上标记更新的内容,
if (
deferRenderPhaseUpdateToNextBatch ||
// 检查我们是否还没有渲染
(executionContext & RenderContext) === NoContext
) {
workInProgressRootInterleavedUpdatedLanes = mergeLanes(
workInProgressRootInterleavedUpdatedLanes,
lane,
);
}
// 根节点已被挂起,意味着当前的渲染不会结束,
// 由于当前又有更新,我们将根节点重新挂起(更新挂起时间)
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
markRootSuspended(root, workInProgressRootRenderLanes);
}
}
// 调度当前根节点,任务过期则立马同步执行
ensureRootIsScheduled(root, eventTime);
if (
lane === SyncLane &&
executionContext === NoContext &&
(fiber.mode & ConcurrentMode) === NoMode &&
// Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
!(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
) {
// 除非我们已经在任务或者批处理中,否则执行同步任务更新
// 不在 scheduleCallbackForFiber 中执行是为了调度一个回调函数的时候不立刻执行
// 只有在用户发起的 update 中使用此方法,以保留遗留模式的历史行为。
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}
return root;
scheduleUpdateOnFiber 主要做了这几件事
接下来进入 ensureRootIsScheduled 函数进行任务的调度。
我们看到函数内部调用了 markUpdateLaneFromFiberToRoot,这个函数主要的作用是更新当前 fiber 节点的lannes,表示当前节点需要更新,然后收集需要更新的子节点的 lane,存放在父 fiber 上的 childLanes 属性上。在后面做更新时,会根据 fiber 节点上 lannes 判断当前 fiber 节点是否需要更新,根据 childLanes 判断当前 fiber 的子节点是否需要更新。
function markUpdateLaneFromFiberToRoot(
sourceFiber: Fiber,
lane: Lane,
): FiberRoot | null {
// 更新当前节点的 lanes,表示当前节点需要更新
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
let alternate = sourceFiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
...
// 从当前需要更新的 fiber 节点向上遍历直到根fiber节点(root fiber),
// 更新每个 fiber 节点的 childLanes
// 在之后会通过 childLanes 来判断当前 fiber 节点下是否有子节点需要更新
let node = sourceFiber;
let parent = sourceFiber.return;
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
} else {
if (__DEV__) {
if ((parent.flags & (Placement | Hydrating)) !== NoFlags) {
warnAboutUpdateOnNotYetMountedFiberInDEV(sourceFiber);
}
}
}
node = parent;
parent = parent.return;
}
if (node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;
return root;
} else {
return null;
}
}
首先将新任务的 lane 当前 fiber 节点上的 lanes 属性,表示当前 fiber 需要更新,如果 fiber 节点对应的alternate 不为空的话,表示是在更新,并且会同步更新 alternate 上的 lanes。
// 更新当前节点的 lanes,表示当前节点需要更新
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
let alternate = sourceFiber.alternate;
if (alternate !== null) {
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
接下来则是从当前更新的节点向上遍历至 fiber 根节点(root fiber),更新每个 fiber 节点上的 childLanes 属性,表示当前 fiber 下的子节点需要更新,遍历更新完成后,则会返回 fiber root节点。
// 从当前需要更新的 fiber 节点向上遍历直到根 fiber 节点(root fiber),
// 更新每个 fiber 节点的 childLanes
// 在之后会通过 childLanes 来判断当前 fiber 节点下是否有子节点需要更新
let node = sourceFiber;
// 获取当前 fiber 节点的父级
let parent = sourceFiber.return;
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
}
node = parent;
parent = parent.return;
}
然后调用了 markRootUpdated 函数,这个函数的作用是将当前需要更新的 lane 添加到 fiber root 的 pendingLanes 属性上,表示有新的更新任务需要被执行,然后将事件触发时间记录在 eventTimes 属性上:
export function markRootUpdated(
root: FiberRoot,
updateLane: Lane,
eventTime: number,
) {
// 将当前需要更新的 lane 添加到 fiber root 的 pendingLanes 属性上
root.pendingLanes |= updateLane;
if (updateLane !== IdleLane) {
root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
}
// 假设 updateLane 为:0b000100
// eventTimes 是这种形式的:[-1, -1, -1, 44573.3452, -1, -1]
// 用一个数组去储存 eventTime,-1表示空位,非-1的位置和 lane 中1的位置相同
const eventTimes = root.eventTimes;
const index = laneToIndex(updateLane);
eventTimes[index] = eventTime;
}
eventTimes 是31位长度的 Array,对应 Lane 使用 31 位的二进制。
markRootUpdated 调用完成后,紧接着调用了 ensureRootIsScheduled 函数,准备开始任务的调度。
在 ensureRootIsScheduled中,scheduleCallback 会以一个优先级调度 render 阶段的开始函 performConcurrentWorkOnRoot,首先判断新任务的优先级是否是同步优先级,是则使用同步渲染模式,否则使用并发渲染模式使用 scheduler 调度任务, 在使用并发模式时,会将 lane 的优先级转换为 React 事件的优先级,然后再根据 React 事件的优先级转换为 Scheduler 的优先级,Scheduler 会根据它自己的优先级给任务做时间分片。
在根节点上调度一个任务,根节点上只能有一个任务,因此如果此根节点已经有一个任务,我们会确保当前任务的优先度和下一个任务的优先度一致。
ensureRootIsScheduled 里面存在了高优先级任务插队和任务饥饿问题,以及批量更新的处理。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// existingCallbackNode 为下一个渲染任务
const existingCallbackNode = root.callbackNode;
// 为当前任务根据优先级添加过期时间
// 并检查未执行的任务中是否有任务过期,有任务过期则 expiredLanes 中添加该任务的 lane
// 在后续任务执行中以同步模式执行,避免饥饿问题
markStarvedLanesAsExpired(root, currentTime);
// 获取最高 lane 等级的新任务的 lane 值
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (nextLanes === NoLanes) {
// 历史任务不为空,
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// newCallbackPriority 实际就是 return_highestLanePriority
// 也就是 nextLanes lane 等级对应的 lane 优先级
const newCallbackPriority = getHighestPriorityLane(nextLanes);
const existingCallbackPriority = root.callbackPriority;
// 新任务和历史任务优先级相同,则继续执行历史任务。
if (
existingCallbackPriority === newCallbackPriority &&
!(
__DEV__ &&
ReactCurrentActQueue.current !== null &&
existingCallbackNode !== fakeActCallbackNode
)
) {
return;
}
if (existingCallbackNode != null) {
// 取消历史任务,scheduler 会安排新的任务
cancelCallback(existingCallbackNode);
}
// 开启一个新的调度
let newCallbackNode;
// 同步任务
if (newCallbackPriority === SyncLane) {
// 执行 scheduleSyncCallback 方法
// 只不过要区分下 legacy 模式还是 concurrent 模式
// scheduleSyncCallback 自己有个 syncQueue, 用来承载同步任务
// 并交由 flushSyncCallbacks 处理这些同步任务后, 再交由下面 scheduleCallback
// 以最高优先级让 Scheduler 调度
if (root.tag === LegacyRoot) {
if (__DEV__ && ReactCurrentActQueue.isBatchingLegacy !== null) {
ReactCurrentActQueue.didScheduleLegacyUpdate = true;
}
// 将同步回调调度到队列中(不会立即执行)
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 将同步回调调度到队列中(不会立即执行)
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
// ImmediateSchedulerPriority 来执行同步任务
if (supportsMicrotasks) {
// 在微任务中执行调度
if (__DEV__ && ReactCurrentActQueue.current !== null) {
ReactCurrentActQueue.current.push(flushSyncCallbacks);
} else {
scheduleMicrotask(flushSyncCallbacks);
}
} else {
// 在立即任务中执行调度
scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
}
newCallbackNode = null;
} else {
// React 事件优先级转换为 Scheduler 优先级
let schedulerPriorityLevel;
// 需要将 lane 转换为 React 事件优先级
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
// 通过 scheduleCallback 将任务及其优先级传入到 Scheduler 中
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
首先我们需要了解什么是饥饿问题: 饥饿问题是指当执行一个任务时,不断的插入多个比该任务优先级高的任务,那么这个任务会一直得不到执行。
任务饥饿问题的处理,主要逻辑在 markStarvedLanesAsExpired 函数中,它主要的作用是为当前任务根据优先级添加过期时间,并检查未执行的任务中是否有任务过期,有任务过期则在 expiredLanes 中添加该任务的 lane,在后续该任务的执行中以同步模式执行,避免饥饿问题:
export function markStarvedLanesAsExpired(
root: FiberRoot,
currentTime: number,
): void {
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const expirationTimes = root.expirationTimes;
// 将要执行的任务会根据它们的优先级生成一个过期时间
// 当某个任务过期了,则将该任务的 lane 添加到 expiredLanes 过期 lanes 上
// 在后续执行任务的时候,会通过检查当前任务的 lane 是否存在于 expiredLanes 上,
// 如果存在的话,则会将该任务以同步模式去执行,避免任务饥饿问题
let lanes = pendingLanes;
while (lanes > 0) {
// 获取当前 lanes 中最左边 1 的位置
// 例如:
// lanes = 28 = 0b0000000000000000000000000011100
// 以 32 位正常看的话,最左边的 1 应该是在 5 的位置上
// 但是 lanes 设置了总长度为 31,所以我们可以也减 1,看作在 4 的位置上
// 如果这样不好理解的话,可以看 pickArbitraryLaneIndex 中的源码:
// 31 - clz32(lanes), clz32 是 Math 中的一个 API,获取的是最左边 1 前面的所有 0 的个数
const index = pickArbitraryLaneIndex(lanes);
// 上面获取到最左边 1 的位置后,还需要获取到这个位置上的值
// index = 4
// 16 = 10000 = 1 << 4
const lane = 1 << index;
// 获取当前位置上任务的过期时间,如果没有则会根据任务的优先级创建一个过期时间
const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
// expirationTimes 与 eventTimes 一样也是 31 位长度的 Array,
// 对应 Lane 使用 31 位的二进制。
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
// 如果有则会判断任务是否过期,过期了则会将当前任务的 lane 添加到 expiredLanes 上
} else if (expirationTime <= currentTime) {
root.expiredLanes |= lane;
}
// 从 lanes 中删除 lane, 每次循环删除一个,直到 lanes 等于 0
// 例如:
// lane = 16 = 10000
// ~lane = 01111
// lanes = 28 = 11100
// lanes = 12 = 01100 = lanes & ~lane
lanes &= ~lane;
}
}
如果一个低优先级的任务执行,并且还在调度的时候触发了一个高优先级的任务,则高优先级的任务打断低优先级任务,此时应该先取消低优先级的任务,因为此时低优先级的任务可能已经进行了一段时间,Fiber 树已经构建了一部分,所以需要将 Fiber 树还原(这个过程发生在函数 prepareFreshStack 中,在这个函数中会初始化已经构建的Fiber 树)。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// 为当前任务根据优先级添加过期时间
// 并检查未执行的任务中是否有任务过期,有任务过期则expiredLanes中添加该任务的lane
// 在后续任务执行中以同步模式执行,避免饥饿问题
markStarvedLanesAsExpired(root, currentTime);
// 获取优先级最高的任务的优先级
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// 如果nextLanes为空则表示没有任务需要执行,则直接中断更新
if (nextLanes === NoLanes) {
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// nextLanes获取的是所有任务中优先级最高任务的lane
// 那么与当前现有的任务的优先级比较,只会有两种结果:
// 1.与现有的任务优先级一样,那么则会中断当前新任务向下的执行,重用之前现有的任务
// 2.新任务的优先级大于现有的任务优先级,那么则会取消现有的任务的执行,优先执行优先级高的任务
// 与现有的任务优先级一样的情况
if (
existingCallbackPriority === newCallbackPriority
) {
return;
}
// 新任务的优先级大于现有的任务优先级
// 取消现有的任务的执行
if (existingCallbackNode != null) {
cancelCallback(existingCallbackNode);
}
// 开始调度任务
// 判断新任务的优先级是否是同步优先级
// 是则使用同步渲染模式,否则使用并发渲染模式(时间分片)
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
...
newCallbackNode = null;
} else {
...
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
scheduleCallback 也就是 unstable_scheduleCallback 方法;根据不同的优先级获取过期时间判断是否执行对应的更新任务,该函数的将会计算任务的开始时间、过期时间、生成任务并将任务放入任务队列。
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
// timerQueue 根据 startTime 排序
// 任务进来的时候, 开始时间默认是当前时间, 如果进入调度的时候传了延迟时间
// 开始时间则是当前时间与延迟时间的和
var startTime;
if (typeof options === "object" && options !== null) {
var delay = options.delay;
if (typeof delay === "number" && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
// taskQueue 根据 expirationTime 排序
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823 (2^30 - 1)
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT; // 10000
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
break;
}
// 计算任务的过期时间, 任务开始时间 + timeout
// 若是立即执行的优先级(IMMEDIATE_PRIORITY_TIMEOUT(-1))
// 它的过期时间是 startTime - 1, 意味着立刻就过期
var expirationTime = startTime + timeout;
// 创建调度任务
var newTask = {
id: taskIdCounter++,
callback, // 调度的任务
priorityLevel, // 任务优先级
startTime, // 任务开始的时间, 表示任务何时才能执行
expirationTime, // 任务的过期时间
sortIndex: -1, // 在小顶堆队列中排序的依据
};
if (enableProfiling) {
newTask.isQueued = false;
}
// startTime > currentTime 说明任务无需立刻执行
// 故放到 timerQueue 中
if (startTime > currentTime) {
// timerQueue 是通过 startTime 判断优先级的,
// 故将 startTime 设为 sortIndex 作为优先级依据
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// 如果 taskQueue 是空的, 并且当前任务优先级最高
// 那么这个任务就应该优先被设为 isHostTimeoutScheduled
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// 如果超时调度已经在执行了, 就取消掉
// 因为当前这个任务是最高优的, 需要先处理当前这个任务
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 设置定时器回调, 定时器触发之后调用
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// startTime <= currentTime 说明任务已过期
// 需将任务放到 taskQueue
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// 如果目前正在对某个过期任务进行调度,
// 当前任务需要等待下次时间片让出时才能执行
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
其实任务的执行时机 = 当前时间 currentime + 延时(delay) + 优先级定时(xxx_PRIORITY_TIMEOUT)
首先任务根据优先级排序,Scheduler 采用 Heap 堆存放任务队列,堆顶就是优先级最高,每次 peek 获取堆顶任务进行执行;在后面的 flushWork 中,会产生一个 while 循环(workLoop)来不断地对队列中的内容进行处理,这期间还会逐步的将被递延任务从 timerQueue 中梳理(advanceTimers)到 taskQueue 中,使得任务能按预设的优先级有序的执行。甚至,对于更高阶的任务回调实现,还可以将任务“分段进行”(continuationCallback)。
根据优先级的高低,shceudler 采用位运算取中位节点的方式,交换 heap 堆中任务的位置。
通过 Shceduler 中 push、peek 添加任务、移除任务,最后构造了两个任务队列 taskQueue 和 timeQueue。
每个任务存放 Heap 堆之前,会根据 sortIndex,id 属性进行优先级排序。
基本获取任务方式:Scheduler 首先 peek (获取)一个 taskQueue 的任务并执行,完成后从任务列表中 pop 移除。执行任务过程中,检查当前时间是否满足 timeQueue 中任务的延期标准,如果满足,将 timeQueue 中的任务移除后放入taskQueue,然后继续循环获取 taskQueue 中的任务。
上面执行 requestHostCallback 时,传入了 flushWork 回调函数,requestHostCallback 只是进行注册任务,并通知调用:
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
他做的工作就是:
接着就是通过监听消息,执行回调函数,也就是 flushWork 函数。
function flushWork(hasTimeRemaining, initialTime) {
// 由于 requestHostCallback 并不一定立即执行传入的回调函数
// 所以 isHostCallbackScheduled 状态可能会维持一段时间
// 等到 flushWork 开始处理任务时, 则需要释放该状态以支持其他的任务被 schedule 进来
isHostCallbackScheduled = false;
// 全局变量,是否有延时任务正在调度(上面在handleTimeout中设置的, 从timerQueue中获取任务,放入taskQueue,这里不需要了)
if (isHostTimeoutScheduled) {
//因为已经进入 flushWork 流程,不需要再执行超时任务,因此取消它
isHostTimeoutScheduled = false;
// 清除定时器(清除 requestHostTimeout 函数设置的定时器)
cancelHostTimeout();
}
// 全局变量,代码执行完成后变成 false
isPerformingWork = true;
// 当前执行任务的优先级
const previousPriorityLevel = currentPriorityLevel;
try {
return workLoop(hasTimeRemaining, initialTime);
} finally {
// 执行完毕后,清除标识
currentTask = null; // 当前任务 null
currentPriorityLevel = previousPriorityLevel; // 恢复 currentPriorityLevel
isPerformingWork = false; // 代码执行结束(正在执行works = false)
}
}
workLoop 承载了任务中断, 任务恢复, 判断任务完成等功能:
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
// 因为是个异步的, 需要再次调整一下 timerQueue 跟 taskQueue
advanceTimers(currentTime);
// 最紧急的过期任务
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused) // 用于 debugger, 不管
) {
// 任务中断
// 时间片到了, 但 currentTask 未过期, 跳出循环
// 当前任务就被中断了, 需要放到下次 workLoop 中执行
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
const callback = currentTask.callback;
if (typeof callback === "function") {
// 清除掉 currentTask.callback
// 如果下次迭代 callback 为空, 说明任务执行完了
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
// 已过期
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
if (enableProfiling) {
markTaskRun(currentTask, currentTime);
}
// 执行任务
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
// 如果产生了连续回调, 说明出现了中断
// 故将新的 continuationCallback 赋值 currentTask.callback
// 这样下次恢复任务时, callback 就接上趟了
if (typeof continuationCallback === "function") {
currentTask.callback = continuationCallback;
if (enableProfiling) {
markTaskYield(currentTask, currentTime);
}
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
// 如果 continuationCallback 不是 Function 类型, 说明任务完成!!!
// 否则, 说明这个任务执行完了, 可以被弹出了
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
// 上面执行任务会消耗一些时间, 再次重新更新两个队列
advanceTimers(currentTime);
} else {
// 上面的 if 清空了 currentTask.callback, 所以
// 如果 callback 为空, 说明这个任务就执行完了, 可以被弹出了
pop(taskQueue);
}
// 如果当前任务执行完了, 那么就把下一个最高优的任务拿出来执行, 直到清空了 taskQueue
// 如果当前任务没执行完, currentTask 实际还是当前的任务, 只不过 callback 变成了 continuationCallback
currentTask = peek(taskQueue);
}
// 任务恢复
// 检查是否还有更多任务
// 时间切片剩余时间到了, 但 taskQueue 还没执行完(也就是任务被中断了)
// 就返回 true, 这就是恢复任务的标志
if (currentTask !== null) {
return true;
} else {
// 若任务完成, 去 timerQueue 中找需要最早开始执行的那个任务
// 进行 requestHostTimeout 调度那一套
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
这个函数用来判断是否需要等待;也就是如果任务超时就进行打断。
function shouldYieldToHost() {
// 通过判断当前时间减去开始时间是否小于 5ms
var timeElapsed = exports.unstable_now() - startTime;
// 小于则返回 false 表示无需中断,
if (timeElapsed < frameInterval) {
// The main thread has only been blocked for a really short amount of time;
// smaller than a single frame. Don't yield yet.
return false;
} // The main thread has been blocked for a non-negligible amount of time. We
return true;
}
接下来就是执行在 scheduleCallback 传入的 performConcurrentWorkOnRoot 函数,进入 render 阶段以及 commit 阶段进行更新;
对于 setState 整体逻辑跟 ReactDOM.render 类似,不过在 ensureRootlsScheduled 调度中,优先级属于同步任务,会通过 queueMicrotask 注册微任务去调度。
这里也要知道 this.setState 是异步的,因为 React 的渲染机制中使用 queueMicrotask 或者 MessageChannel 将更新任务添加进微任务队列或者宏任务队列中以此实现异步渲染。至于为什么要使用异步渲染。以为在 React 中会将连续调用的 setState 进行批量更新,这样做的目的,是为了避免短时间内连续调用造成不必要的渲染,增加性能的开销;批量更新只会在一个微任务或宏任务中进行。比如下面例子:
this.state = {num: 0};
function onClick() {
this.setState({
num: this.state.num + 1
});
this.setState({
num: this.state.num + 2
});
}
其实点击完,this.state.num 结果为 2,在 ensureRootlsScheduled 源码可以看到,在 React 合成事件中连续调用的 setState 的优先级是一样的,在第一个 setState 调用后,再调用第二个时,会将第一个更新任务的优先级与第二个更新任务的优先级进行比较,如果优先级一样,则不会执行第二个更新任务,而是将第二个任务的更新内容与第一个的更新内容进行合并,最终只会进行一次更新渲染,这样的做法叫做批量更新。
这样做的目的,是为了避免短时间内连续调用 setState 造成不必要的渲染,增加性能的开销。
https://juejin.cn/post/7008802041602506765
https://zhuanlan.zhihu.com/p/384525799
https://react.iamkasong.com/state/prepare.html
https://segmentfault.com/a/1190000022942008