React 的源码与原理解读(九):Lanes
写在专栏开头(叠甲)
-
作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
-
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
-
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
本一节的内容
在之前的章节中,我们主要分析了 **Scheduler ** 系统是怎么样调度我们的渲染任务的,他主要用于判断在一批任务的执行过程中,我们什么时候要把进程还给我们的浏览器;而这一篇我们要谈到我们 React 中的了一个套优先级系统 —— lanes,这套系统主要用于我们讨论优先级低的任务执行过程中是不是一个被打断,转而执行优先级更高的任务。
Lanes 的定义
我们首先来看看这个 lane 的定义,他在代码的这个位置: packages/react-reconciler/src/ReactFiberLane.new.js:
可以看到 lanes 使用31位二进制来表示优先级车道,共31条, 位数越小(1的位置越靠右)表示优先级越高。在实际使用中,我们会有一个 31位的二进制数来标识我们的任务,如果他的某一位对应的是 1 ,那么他的某个优先级就有任务了,反之就是空闲的,你可以理解成我们把任务看成我一辆辆汽车,他们需要在不同的车道上行驶才不会相互影响,但是右边的车道可以向左边超车(优先级高)。这就是为什么我们称之为 车道模型。
注意:后文中,我们将用 lane 表示单个优先级,也就是一个任务占用的某个车道;lanes 表示我们所有车道,lanes 中的每个车道对应一个 lane
// 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: Lane = /* */ 0b0000000000000000000000000000100;
// 默认优先级,例如使用setTimeout,请求数据返回等造成的更新
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000010000;
// 过度优先级,例如: Suspense、useTransition、useDeferredValue等拥有的优先级(React18 的新机制)
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;
//非空闲车道
const NonIdleLanes: Lanes = /* */ 0b0001111111111111111111111111111;
// 空闲车道
export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0100000000000000000000000000000;
// 屏幕外车道
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
Lanes 的操作
lane 之所以这么设计,主要是方便我们使用位操作来实现一些比较、合并、重置等功能,我们来看看一些常用的操作,在后续都会涉及:
- 生成 lane
我们可以使用左移操作来快速生成一个对应的 lane,比如我们想要生成一个 DefaultLane
我们只需要用下面的代码即可:
const lane = 1 << 5;
- 筛选 lanes
我们有时候需要判断哪些 lane 已经有任务了,哪些 lane 没有任务,这个时候我们需要使用我们的 lanes 和我们预定义的一些 lanes 内容进行按位与的操作,得到的内容就是我们哪些对应的车道上有任务,比如我们现在想要知道在非闲置车道上哪些车道已经有任务了,我们只需要用 NonIdleLanes
和我们 lanes 做按位与操作即可
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
如果我们想要筛选出不包含某些车道的任务,我们只需要先把筛选模型按位取反再按位与即可:
const IdlePendingLanes = pendingLanes & ~NonIdleLanes;
- 合并 lanes
如果我们把我们的 lane 合并到 lanes 中,我们只需要进行按位或的操作就行了,所有标识为 1 的部分都会自动合并,这用于我们将我们当前任务使用的车道合并到总的 lanes 中:
root.pendingLanes |= updateLan
- 取出优先级最高的已经使用的 lane
取出优先级最高的任务,也就是二进制中最右边的那个 1。操作是 lanes 与自己的负数进行按位与操作,因为我们的负数是使用补码保存的,如果最后一位是 1 ,那么补码就会变成 1,与操作就会返回 1;如果最后一位是 0 ,补码就是 0,但是会产生一位的进位,这个进位会持续到遇到 0 为止,而这个 0 就是 1的补码,此时就会产生 1 了,与操作就会返回 1,你可以写几个数自己推一下这个结论:
lanes & -lanes;
- 删除对应的 lane
我们有时候会从我们总的 lanes 里取出我们需要 lane 的然后重置它,我们可以对指定的车道的值按位取反,然后和我们的 lanes 进行按位与操作,这时除了我们需要的那个车道是 0 其他都是 1 ,按位与的时候,我们重置的车道必定变成 0,而其他的车道会变成保留自己原来的值:
lanes &= ~lane;
Fiber 中的 Lanes
在我们开始正式讲解之前,我们先来看看我们之前省略的 Fiber 上的和 lanes 相关的结构,他和我们要讲解的整个流程息息相关:
首先是 Fiber 节点上的两个结构:
lanes 存储的是这个 节点的 lanes,而 childLanes 存储的是 孩子节点的 lanes,我们通过这样的结构一层一层的将我们的更新任务传递上去,我们可以通过这个 childLanes 快速知道我们的子树是不是需要更新
lanes: Lanes,
childLanes: Lanes,
之后是我们的 FiberRoot 节点,我们来看看其中的一些和 lanes 相关的属性,这里你只需要一个大概的映像即可,我们之后会具体来说,其中 suspencedLanes 和 pingedLanes 两个是用于 suspense 类型组件的,也就是异步渲染的,有兴趣的可以自己去了解一下,我们不会详细来说,我们主要用到 pendingLanes 和 expiredLanes 两个字段:
callbackPriority = NoLane; // 传入的回调函数的优先级
eventTimes = createLaneMap(NoLanes); // 每个lane 的事件发生的事件
pendingLanes = NoLanes // 即将被处理的 lanes 列表。
suspencedLanes = NoLanes; // 被挂起的 lanes 列表,React 通过 suspense 和 lazy 这两个功能来支持异步渲染
pingedLanes = NoLanes; // 记录需要重新安排或处理的 lanes ,和上面的 suspencedLanes 配合使用
expiredLanes = NoLanes; // 表示过期任务的 lanes
mutableReadLanes = NoLanes; // 可变状态的优先级的 lanes
entangledLanes = NoLanes; // 当前正在执行的任务需要等待哪些任务完成
事件 Lane 的判定
那么现在我们来看 lane 是怎么判定的,也就是什么时候使用什么 lane,我们先回到 createRoot 说起,在 createRoot 中有一个函数 listenToAllSupportedEvents
,它的作用是绑定所有可支持的事件。
// 绑定所有可支持的事件
listenToAllSupportedEvents(rootContainerElement);
我们来看看其中的逻辑,我们只看 lanes 的部分,其中进入了 listenToNativeEvent
这个函数中,这个函数又调用了 addTrappedEventListener
这个函数;这个函数调用了 createEventListenerWrapperWithPriority
,我们之后仔细来看它
export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
//... 省略
listenToNativeEvent(domEventName, true, rootContainerElement);
}
export function listenToNativeEvent(
domEventName: DOMEventName,
isCapturePhaseListener: boolean,
target: EventTarget,
): void {
addTrappedEventListener(
target,
domEventName,
eventSystemFlags,
isCapturePhaseListener,
);
}
function addTrappedEventListener(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
isCapturePhaseListener: boolean,
isDeferredListenerForLegacyFBSupport?: boolean,
) {
let listener = createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags,
);
}
createEventListenerWrapperWithPriority
这个函数调用了 getEventPriority
这个函数,它将传入的事件根据类别设定了优先级,这个优先级和我们 lane 里面的优先级是一一对应的:
- 用户点击,input框输入等都设置为同步优先级,这是因为用户在操作的时候需要立即得到反馈,如果操作完没有反馈就会给用户造成界面卡顿的感觉。
- 滚动事件,拖动事件等连续触发的事件我们设置连续触发优先级
- 如果是 message 事件,我们通过调用当前任务的优先级来判定任务的优先级
- 其他事件就是默认优先级
在获取优先级后,会根据获取到的事件的优先级相对应优先级的回调函数
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,
);
}
// 离散事件优先级,例如:点击事件,input输入等触发的更新任务,优先级最高
export const DiscreteEventPriority: EventPriority = SyncLane;
// 连续事件优先级,例如:滚动事件,拖动事件等,连续触发的事件
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
// 默认事件优先级,例如:setTimeout触发的更新任务
export const DefaultEventPriority: EventPriority = DefaultLane;
// 闲置事件优先级,优先级最低
export const IdleEventPriority: EventPriority = IdleLane
//获取事件的优先级
export function getEventPriority(domEventName: DOMEventName): * {
switch (domEventName) {
case 'cancel':
case 'click':
//... 省略
case 'selectstart':
return DiscreteEventPriority;
case 'drag':
//... 省略
case 'pointerleave':
return ContinuousEventPriority;
case 'message': {
const schedulerPriority = getCurrentSchedulerPriorityLevel();
switch (schedulerPriority) {
case ImmediateSchedulerPriority:
return DiscreteEventPriority;
case UserBlockingSchedulerPriority:
return ContinuousEventPriority;
case NormalSchedulerPriority:
case LowSchedulerPriority:
return DefaultEventPriority;
case IdleSchedulerPriority:
return IdleEventPriority;
default:
return DefaultEventPriority;
}
}
default:
return DefaultEventPriority;
}
}
Lane 的获取
在上文中我们已经提到了不同的事件会给 React 判定为不同的 lane,绑定给这个事件,那么这个优先级是怎么起作用的呢,我们从一次更新的创建说起,我们先来看看我们的一个事件调用 setState 创建更新时发生了什么:
首先当我们调用 setState
的时候,我们调用了 enqueueSetState
这个函数:
Component.prototype.setState = function(partialState, callback) {
// ... 省略
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
enqueueSetState
给事件对应的 fiber 创建一个 update,然后将该 update 对象添加到 updateQueue 更新队列中,这部分看过前面教程的读者应该非常熟悉,和我们第四篇中 updateContainer
的逻辑基本完全一致,当时我们跳过没有详细讲优先级的部分,这篇我们具体来说,这里调用了 requestUpdateLane
来获取优先级
enqueueSetState(inst, payload, callback) {
// 获得 fiber
const fiber = getInstance(inst);
// 获取当前事件触发的时间
const eventTime = requestEventTime();
// 获取到当前事件对应的 Lane
const lane = requestUpdateLane(fiber);
// 创建更新对象
const update = createUpdate(eventTime, lane);
// 挂载
update.payload = payload;
if (callback !== undefined && callback !== null) {
//省略 DEV 模式代码
update.callback = callback;
}
// 将更新对象添加进更新队列中
enqueueUpdate(fiber, update, lane);
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
if (root !== null) {
entangleTransitions(root, fiber, lane);
}
// ....
if (enableSchedulingProfiler) {
markStateUpdateScheduled(fiber, lane);
}
},
requestUpdateLane
函数的作用是根据当前的模式和当前的事件来判断其优先级,它从上到下的逻辑是这样的:
- 首先判断是不是同步模式( React 18 默认是并发模式),同步模式不需要判断优先级,直接返回同步优先级
- 之后判定是不是有任务在执行了, 也就是判断
workInProgressRootRenderLanes
是不是空的。这个我们之后会详细提到,如果有的话直接返回其优先级 - 之后判定是不是过渡优先级,这是 React 18 新增加的机制
Transition
,它的作用是可以手动降低一些更新任务的优先级,使得它可以在稍后触发来保证页面的一些事件可以即使得到反馈。结合上面的介绍,我们可以看到我们定义的Transition
优先级都是在默认优先级之后的部分,也就是优先级较低的部分。 - 过渡优先级分配的逻辑是,我们先使用优先级最高的过渡优先级 TransitionLane1 ,如果已经存在了这个优先级,那么我们继续使用 TransitionLane2,直到 16个 TransitionLane 都被使用完毕,然后重新从第一位 TransitionLane1 开始使用
- 如果不是过渡优先级,说明处于空闲状态下,我们要正常开始一个任务,我们取出上文中,根据我们绑定的事件设置的优先级进行操作
- 如果它不是由我们的事件绑定的触发的优先级,那么判定它为外部事件触发,比如 setTimeout等,获取其设置的优先级
export function requestUpdateLane(fiber: Fiber): Lane {
// 获取到当前渲染的模式:sync mode(同步模式) 还是 concurrent mode(并发模式)
const mode = fiber.mode;
if ((mode & ConcurrentMode) === NoMode) {
// 同步模式
return (SyncLane: Lane);
} else if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
//并发模式,判断是否有任务正在执行,workInProgressRootRenderLanes是在初始化workInProgress树时,将当前执行的任务的优先级赋值给了workInProgressRootRenderLanes,如果 workInProgressRootRenderLanes 不为空说明有任务在执行了,那么则直接返回这个正在执行的任务的lane,
return pickArbitraryLane(workInProgressRootRenderLanes);
}
// 是否是过渡优先级,如果是的话,则返回一个过渡优先级
const isTransition = requestCurrentTransition() !== NoTransition;
if (isTransition) {
if (__DEV__ && ReactCurrentBatchConfig.transition !== null) {
const transition = ReactCurrentBatchConfig.transition;
if (!transition._updatedFibers) {
transition._updatedFibers = new Set();
}
transition._updatedFibers.add(fiber);
}
if (currentEventTransitionLane === NoLane) {
currentEventTransitionLane = claimNextTransitionLane();
}
return currentEventTransitionLane;
}
// 获取我们上文提到的绑定的不同事件的对应优先级
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
return updateLane;
}
// 在react的外部事件中触发的更新事件,比如:setTimeout等,会在触发事件的时候为当前事件设置一个优先级,可以直接拿来使用
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;
}
schedule 阶段 Lane 的作用
上文中,我们获取了一个更新事件的 lane ,之后我们再来详细看看,这个获取的 lane 怎么发挥它的作用:
我们顺着 enqueueSetState
函数继续向下寻找,在获取了 lane 之后,我们需要进行创建一个 update 对象放到跟新队列中,之后我们调用了 scheduleUpdateOnFiber
函数,这个函数我们之前已经提过一次了,但是我们只是提了一下其中的渲染逻辑,现在我们加上优先级相关的内容再来看看这个函数:
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
): FiberRoot | null {
// 检查是否做了无限循环更新,弱是则抛出异常
checkForNestedUpdates();
// 自底向上更新整个优先级
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root === null) {
return null;
// 将当前需要更新的lane添加到fiber root的pendingLanes属性上,表示有新的更新任务需要被执行
// 通过计算出当前lane的位置,并添加事件触发时间到eventTimes中
markRootUpdated(root, lane, eventTime);
// ....
ensureRootIsScheduled(root, eventTime);
// ....
return root;
}
首先我们调用了 markUpdateLaneFromFiberToRoot
这个函数,它的作用是更新优先级,我们来看看它的操作:
- 首先我们将我们的优先级合并到 fiber 上
- 之后判定有没有另一颗树(因为Raect 是双缓存结构),如果有,要把内容也同步上去
- 之后寻找节点的父节点,把这个优先级合并到父节点上
- 之后再寻找其父节点的父节点,自顶向上,直到同步到根节点为止
function markUpdateLaneFromFiberToRoot(
sourceFiber: Fiber,
lane: Lane,
): FiberRoot | null {
sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane); // 将传入的lane合并到 fiber 节点上
let alternate = sourceFiber.alternate; // 另一个fiber树,若是在调用render()初始化时,alternate为null
if (alternate !== null) {
// 若alternate不为空,说明是更新节点,这里将另一棵树也更新优先级
alternate.lanes = mergeLanes(alternate.lanes, lane);
}
// ... 省略DEV
// 从父节点到根节点,更新的childLanes字段,表示这个节点的子节点有lane的更新
let node = sourceFiber;
let parent = sourceFiber.return;
while (parent !== null) {
parent.childLanes = mergeLanes(parent.childLanes, lane);
alternate = parent.alternate;
if (alternate !== null) {
// 若另一棵fiber树不为空,则同时更新另一棵树的childLanes
alternate.childLanes = mergeLanes(alternate.childLanes, lane);
} else {
// alternate为空,说明是mount阶段,不用管
// ....
}
node = parent;
parent = parent.return;
}
// 当parent为空时,node正常情况下是 HostRoot 类型,返回node节点的stateNode,即FiberRootNode
if (node.tag === HostRoot) {
const root: FiberRoot = node.stateNode;
return root;
} else {
// 若node不是 HostRoot 类型,可能的情况时,该fiber树是独立于react单独构建出来的
return null;
}
}
紧接着我们调用了 markRootUpdated
函数,这个函数将当前需要更新的 lane 合并到 fiber root 的 pendingLanes 属性上,这个属性我们之前讲了,表示等待执行的 lanes ,然后将事件触发时间记录在 eventTimes 属性上。
其中 eventTimes 是 31 位长度的数组,和 lanes 的 31 位相对应,不同优先级的任务的触发时间将被记录到 eventTimes 的不同位置上
export function markRootUpdated(
root: FiberRoot,
updateLane: Lane,
eventTime: number,
) {
root.pendingLanes |= updateLane;
if (updateLane !== IdleLane) {
root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
}
// root.eventTimes和root.lanes两个数组一一对应,-1表示空位,非-1的位置和lane中1的位置相同
const eventTimes = root.eventTimes;
const index = laneToIndex(updateLane);
eventTimes[index] = eventTime;
}
之后我们进入到 ensureRootIsScheduled
函数中,这个函数调度我们的任务,我们来看看其中做了什么处理,还是一样,我们跳过已经讲解的部分,只看我们的 lanes 部分:
- 我们首先判定刚刚放在 pendingLanes 中的任务,有没有要过期的(我们记录了触发时间),如果有马上要过期的就取出它,设置高优先级,保证它尽快执行,这部分我们后面会展开说
- 之后我们确定下一个一批 lane ,如果没有车道要执行,说明我们可以执行的任务执行完了,结束我们的任务调度
- 如果有任务要执行,我们获取其中优先级最高的一个,如果新任务比正在执行的任务的优先级低,那么则不会去管它,继续渲染,反之,新任务的优先级比正在执行的任务高,那么则取消当前任务,先执行新任务
- 最后根据这个任务的优先级(同步优先级还是其他)来判断调用什么方法
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
/**
* 判断 pendingLanes 中的任务,是否有过期的,
* 若该任务没有设置过期时间,则根据该任务的lane设置过期时间,
* 若任务已过期,则将该任务放到 expiredLanes 中,表示马上就要执行,
* 在后续任务执行中以同步模式执行,避免饥饿问题
*/
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;
}
// 获取这批任务中优先级最高的一个
const newCallbackPriority = getHighestPriorityLane(nextLanes);
const existingCallbackPriority = root.callbackPriority;
if (
existingCallbackPriority === newCallbackPriority &&
!(
__DEV__ &&
ReactCurrentActQueue.current !== null &&
existingCallbackNode !== fakeActCallbackNode
)
) {
//....
// 若新任务的优先级与现有任务的优先级一样,则继续正常执行之前的任务
return;
}
// 新任务的优先级大于现有的任务优先级,取消现有的任务的执行
if (existingCallbackNode != null) {
cancelCallback(existingCallbackNode);
}
// 开始调度任务,判断新任务的优先级是否是同步优先级
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
if (root.tag === LegacyRoot) {
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
//.....
newCallbackNode = null;
} else {
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
下面我们具体来看看每个函数,markStarvedLanesAsExpired
函数的作用是设定每个 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;
let lanes = pendingLanes;
while (lanes > 0) {
// 获取当前lanes中最左边1的位置
const index = pickArbitraryLaneIndex(lanes);
// 根据位置还原 lane
const lane = 1 << index;
// 获取当前位置上任务的过期时间
const expirationTime = expirationTimes[index];
// 没过期时间,根据优先级添加一个过期时间
if (expirationTime === NoTimestamp) {
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
// 过期了,添加到过期队列中,这个队列的任务需要尽快执行防止饥饿
root.expiredLanes |= lane;
}
// 从lanes中删除lane, 每次循环删除一个,直到lanes等于0
lanes &= ~lane;
}
}
getNextLanes
函数的作用是找出我们要执行的优先级最高的任务:
- 首先判定有没有闲置任务,方法是和
NonIdleLanes
按位与,得到的不是 0 代表有未闲置任务,闲置任务的车道是最高位的几条,所以优先级最低,我们要最后才处理 - 之后和我们的 suspendedLanes 操作去除其中挂起的任务,被挂起的任务要等其他任务执行完毕后才能执行
- 如果没有未挂起的任务了,我们找出所有的挂起任务,按照优先级执行挂起任务
- 如果未闲置任务都执行完了,我们再按照优先级执行闲置任务,它也是按照先未挂起任务再挂起任务的顺序执行的
- 如果正在渲染,突然新添加了一个任务,但是这个新任务比正在执行的任务的优先级低,那么则不会去管它,继续渲染,如果新任务的优先级比正在执行的任务高,那么则取消当前任务,执行新任务,返回
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
// 没任务了,跳出
const pendingLanes = root.pendingLanes;
if (pendingLanes === NoLanes) {
return NoLanes;
}
let nextLanes = NoLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
//在将要处理的任务中检查是否有未闲置的任务,如果有的话则需要先执行未闲置的任务
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
if (nonIdlePendingLanes !== NoLanes) {
// 去除挂起的任务
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
if (nonIdleUnblockedLanes !== NoLanes) {
// 有未挂起的任务,获取最高等级的任务
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
} else {
// 都挂起了,从挂起的任务中找到优先级最高的来执行
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
}
}
} else {
// 剩下的任务都是闲置的,判断逻辑相同
const unblockedLanes = pendingLanes & ~suspendedLanes;
if (unblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(unblockedLanes);
} else {
if (pingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(pingedLanes);
}
}
}
// 从pendingLanes中找不到有任务,则返回一个空
if (nextLanes === NoLanes) {
return NoLanes;
}
// wipLanes是正在执行任务的lanes,nextLanes是本次需要执行的任务的lanes,新任务比正在执行的任务的优先级低,那么则不会去管它,继续渲染,反之,新任务的优先级比正在执行的任务高,那么则取消当前任务,先执行新任务:
if (
wipLanes !== NoLanes &&
wipLanes !== nextLanes &&
(wipLanes & suspendedLanes) === NoLanes
) {
const nextLane = getHighestPriorityLane(nextLanes);
const wipLane = getHighestPriorityLane(wipLanes);
if (
nextLane >= wipLane ||
(nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
) {
return wipLanes;
}
}
Reconciliation 阶段 Lane 的作用
之后我们进入到 performConcurrentWorkOnRoot
函数中来看看 lanes 又是怎么样作用的:
- 我们又调用了一次
getNextLanes
方法,获取当前优先级最高的一批任务,因为在执行任务前,可能又产生了一个新的任务,我们要保证在任何时候都要保证执行的任务是优先级最高的。 - 使用
shouldTimeSlice
函数来判定需要使用什么模式渲染,如果任务已经超时,我们使用同步模式渲染(因为同步模式不会被打断,所以解决了饥饿的问题),否则根据当前优先级和渲染模式决定我们使用并发模式还是同步模式渲染
function performConcurrentWorkOnRoot(root, didTimeout) {
// 再获取一次 lanes
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (lanes === NoLanes)
return null;
}
// shouldTimeSlice 函数根据 lane 的优先级,决定是使用并发模式还是同步模式渲染(解决饥饿问题)
// didTimeout判断当前任务是否是超时
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes); // React18初始化时走的是这里
// 省略 .... (失败处理)
ensureRootIsScheduled(root, now());
if (root.callbackNode === originalCallbackNode) {
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}
export function shouldTimeSlice(root: FiberRoot, lanes: Lanes) {
// 任务已经过期了,我们使用同步模式渲染
if ((lanes & root.expiredLanes) !== NoLanes) {
return false;
}
// 检查当前是否开启并发模式和当前使用的渲染模式是否是并发模式
if (
allowConcurrentByDefault &&
(root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
) {
return true;
}
// 这四个lane都是需要使用同步模式执行的
const SyncDefaultLanes =
InputContinuousHydrationLane |
InputContinuousLane |
DefaultHydrationLane |
DefaultLane;
// 根据优先级判定使用并发还是同步模式渲染
return (lanes & SyncDefaultLanes) === NoLanes;
}
我们进行深入,进入 renderRootConcurrent
方法,其中调用了 prepareFreshStack
函数,在这个函数里,我们将传入的当前任务的优先级给了 workInProgressRootRenderLanes
、subtreeRenderLanes
、workInProgressRootIncludedLanes
属性:
- **workInProgressRootRenderLanes **表示当前是否有任务正在执行,有值则表示有任务正在执行,反之则没有任务在执行。
- **subtreeRenderLanes **表示需要更新的 fiber 节点的 lane 的集合,因为我们是,在后面更新 fiber 节点的时候会根据这个值判断是否需要更新。
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
root.finishedWork = null;
root.finishedLanes = NoLanes;
workInProgressRoot = root;
const rootWorkInProgress = createWorkInProgress(root.current, null);
workInProgress = rootWorkInProgress;
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
return rootWorkInProgress;
}
之后我们一路深入直到我们的 beginWork
函数,checkScheduledUpdateOrContext
方法来判断是不是可以复用之前的节点,这个函数里,如果可以复用就不继续执行了,如果不能复用会用继续 beginWork
的逻辑,也就是我们之前讲过的分类讨论处理每种不一样的节点:
function performUnitOfWork(unitOfWork: Fiber): void {
// subtreeRenderLanes 标识需要更新的点集合
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
//...
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
// 新旧的 props 是不是相同
if (
oldProps !== newProps ||
//....
) {
// 如果不同
didReceiveUpdate = true;
} else {
// 如果相同
// checkScheduledUpdateOrContext 函数检查当前 fiber 节点上的 lanes 是否存在 subtreeRenderLanes 中
const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
current,
renderLanes,
);
if (
!hasScheduledUpdateOrContext &&
(workInProgress.flags & DidCapture) === NoFlags
) {
didReceiveUpdate = false;
// 复用之前的节点
return attemptEarlyBailoutIfNoScheduledUpdate(
current,
workInProgress,
renderLanes,
);
}
//....
}
} else {
didReceiveUpdate = false;
}
//...
}
//判断能不能复用
function checkScheduledUpdateOrContext(
current: Fiber,
renderLanes: Lanes,
): boolean {
const updateLanes = current.lanes;
// 当前节点的 lanes 是不是在 subtreeRenderLanes 中
if (includesSomeLane(updateLanes, renderLanes)) {
return true;
}
if (enableLazyContextPropagation) {
const dependencies = current.dependencies;
if (dependencies !== null && checkIfContextChanged(dependencies)) {
return true;
}
}
return false;
}
之后,我们在后续操作中,我们会使用 processUpdateQueue
操作我们的更新队列,这个我们在之前也提到过,其中涉及到了 lane 的变换,我们来看看:这段代码主要是在循环取出当前 fiber 节点上的 updateQueue
中的更新对象,然后根据更新对象上挂载的lane与renderLanes 比较,判断更新对象上的 lane 是否存在于 renderLanes
上,如果存在,则表示当前更新对象需要更新,如果不存在,则会重用之前的状态,跳过该更新对象。
export function processUpdateQueue<State>(
workInProgress: Fiber,
props: any,
instance: any,
renderLanes: Lanes,
): void {
let update = firstBaseUpdate;
do {
const updateLane = update.lane;
const updateEventTime = update.eventTime;
// 判断 updateLane 是否在 renderLanes(subtreeRenderLanes) 上,
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 若当前 update 的操作的优先级不够。跳过此更新
//.....
// 更新优先级
newLanes = mergeLanes(newLanes, updateLane);
} else {
// 执行更新
}
} while (true);
}
Commit 阶段 Lane 的作用
在更新完毕后,最后我们来看看 commit 阶段关于优先级的操作:对于每个节点,他将直接的和孩子中还需要处理的 lane 合并,得到我们还需要执行的 lanes ,从我们的 pendingLanes
中去掉这部分就是我们已经执行完的 lanes ,我们从我们的 Fiber Root 中删除所有已经执行完的 lane,然后把剩余的还需要执行的部分挂载回去用于我们下一次更新,最后我们再次开启我们的更新执行这部分 lane:
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<mixed>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority,
) {
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
root.callbackPriority = NoLane;
// 获取到剩下还需要做更新的lanes
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
// 清理掉已经更新的 lanes
markRootFinished(root, remainingLanes);
remainingLanes = root.pendingLanes;
if (remainingLanes === NoLanes) {
legacyErrorBoundariesThatAlreadyFailed = null;
}
// .....
}
export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
// 从 pendingLanes 中删除还未执行的lanes,那么就找到了已经执行过的lanes
const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
// 将剩下的lanes重新挂载到pendingLanes上,准备下一次的执行
root.pendingLanes = remainingLanes;
root.suspendedLanes = 0;
root.pingedLanes = 0;
// 从expiredLanes, mutableReadLanes, entangledLanes中删除掉已经执行的lanes
root.expiredLanes &= remainingLanes;
root.mutableReadLanes &= remainingLanes;
root.entangledLanes &= remainingLanes;
const entanglements = root.entanglements;
const eventTimes = root.eventTimes;
const expirationTimes = root.expirationTimes;
// 取出已经执行的lane,清空它们所有的数据, eventTimes中的事件触发时间,expirationTimes中的任务过期时间等
let lanes = noLongerPendingLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
entanglements[index] = NoLanes;
eventTimes[index] = NoTimestamp;
expirationTimes[index] = NoTimestamp;
lanes &= ~lane;
}
}
总结
至此我们的 lane 的处理讲完了,我们来总结一下整个过程:
- 首先我们编写代码绑定事件的时候,根据我们的不同事件,React 回给他提供一个 lane
- 之后当我们出发这个事件产生更新的时候,我们可以获取到这个事件的优先级,把产生的更新放到对应 Fiber 的更新队列中
- 这个更新产生后,我们对应 Fiber 节点产生了变化,开始了新的调度,我们要自底向上更新我们的优先级,通过改变 Fiber 的
lane
和父节点的childLanes
,这些更新最后会被收集到 Fiber root 的pendingLanes
中,然后将事件的触发事件更新到eventTimes
中 - 之后我们开始我们的任务调度,每次先判断
pendingLanes
中有没有要过期的任务,如果有任务过期了将它放到expiredLanes
紧急队列中尽快调度防止它饥饿,如果任务没设置过过期事件我们则跟他的 lane 给他设置过期事件 (优先级越高过期越快) - 之后我们根据
expiredLanes
和pendingLanes
以及我们的suspencedLanes
取得我们当前的最紧急的任务,根据优先级判断当前最紧急的任务能不能打断我们现有任务的执行(高优先级抢占),然后根据我们任务的优先级判断我们用什么模式开始我们的任务,我们把计算得到的优先级最高的一批任务放到subtreeRenderLanes
中,作为这次调度的执行任务 - 之后我们进行一次 DIFF 操作,操作中,我们根据对应 Fiber 上的 lanes 是不是在
subtreeRenderLanes
中判断是不是可以直接复用 - 如果不能复用,我们回调用
processUpdateQueue
操作我们 Fiber 上的更新队列,我们根据 fiber 上的 lanes 是不是在subtreeRenderLanes
判断是不是可以开始这个更新,如果一个任务更新了,我们就删除对应的 lane - 在 DIFF 流程结束后,我们进入 commit 阶段,我们将每个节点和孩子节点的 lanes 合并,计算出我们的还没有更新的 lanes 和已经更新的 lanes ,从 root 中删除已经更新的 lanes ,把还没有更新的 lanes 作为我们下一次调度的任务,然后开始一次新的调度,循环往复,直到我们的任务都运行完毕
以上就是 lane 模型在我们整个 React 的更新渲染过程中发挥的作用,讲完这篇之后,我们整个 React 的更新渲染从更新任务产生到调度到怎么样实现更新渲染都有了详细的了解,之后我们回来看一些高级的特性,下一篇可能会从 Hooks 开说起,敬请关注!