概述
Lane是React中用于表示任务的优先级。优先级分为高优先级与低优先级,当用户操作界面时,为了避免页面卡顿,需要让出线程的执行权,先执行用户触发的事件,这个我们称之为高优先级任务,其它不那么重要的事件我们称之为低优先级任务。
不同优先级的任务间,会存在一种现象:当执行低优先级任务时,突然插入一个高优先级任务,那么会中断低优先级的任务,先执行高优先级的任务,我们可以将这种现象称为任务插队。当高优先级任务执行完,准备执行低优先级任务时,又插入一个高优先级任务,那么又会执行高优先级任务,如果不断有高优先级任务插队执行,那么低优先级任务便一直得不到执行,我们称这种现象为任务饥饿问题。
不同的优先级机制
React中有三套优先级机制:
- React事件优先级
- Lane优先级
- Scheduler优先级
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机制耦合,后面事件优先级有什么变动的话,可以直接修改而不会影响到Lane。
Lane优先级转换为React事件优先级:
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;
}
Scheduler优先级
export const NoPriority = 0; //没有优先级
export const ImmediatePriority = 1; // 立即执行任务的优先级,级别最高
export const UserBlockingPriority = 2; // 用户阻塞的优先级
export const NormalPriority = 3; // 正常优先级
export const LowPriority = 4; // 较低的优先级
export const IdlePriority = 5; // 优先级最低,闲表示任务可以闲置
React事件优先级转换为Scheduler优先级
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优先级
// 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的位置越靠右)表示优先级越高。
上面简单的介绍了一下React中的优先级机制,下面我们正式开启源码解析。
如何为React不同的事件添加不同的优先级
当我们触发一个React事件产生一个任务时,它会自带一个优先级,那么这个优先级是怎么赋予这个事件的?
当我们项目第一次渲染时:
const root = document.getElementById('root');
ReactDOM.createRoot(root).render(<div>Hello World</div>);
调用createRoot方法创建根节点后,会为root这个节点做事件委托:
export function createRoot(
container: Container,
options?: CreateRootOptions,
): RootType {
...
// 创建容器-Fiber根节点
const root = createContainer(
container,
ConcurrentRoot,
hydrate,
hydrationCallbacks,
isStrictMode,
concurrentUpdatesByDefaultOverride,
);
// 在root容器上添加事件监听,做事件委托
listenToAllSupportedEvents(rootContainerElement);
}
也就是在这个时候,会对所有支持的事件做一个优先级分类,并赋予这些事件不同的优先级:
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,并且都设置了当前事件相对应的事件优先级的值。
Lane是如何在React中工作的
当我们触发一个点击事件调用setState产生一个更新任务的时候:
Component.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
首先调用setState函数发起更新,setState内部调用了enqueueSetState函数:
enqueueSetState(inst, payload, callback) {
const fiber = getInstance(inst); //获取当前组件对应的fiber节点
const eventTime = requestEventTime(); // 获取当前事件触发的时间
const lane = requestUpdateLane(fiber); // 获取到当前事件对应的Lane优先级
// 创建更新对象,将需要更新的内容挂载到payload上
const update = createUpdate(eventTime, lane);
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
// 将更新对象添加进更新队列中
enqueueUpdate(fiber, update, lane);
const root = scheduleUpdateOnFiber(fiber, lane, eventTime);
...
}
获取事件的优先级
首先获取到当前需要更新的组件的fiber对象,然后调用了requestUpdateLane函数获取到了当前事件的优先级,我们来看一下requestUpdateLane函数内部是如何获取到事件优先级的:
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;
}
非concurrent模式
首先会检查当前的渲染模式是否是concurrent模式,如果不是concurrent模式则都会使用同步优先级做渲染:
if ((mode & ConcurrentMode) === NoMode) {
return (SyncLane: Lane);
}
concurrent模式
如果是,则会接着检查当前是否有任务正在执行,workInProgressRootRenderLanes是在初始化workInProgress树时,将当前执行的任务的优先级赋值给了workInProgressRootRenderLanes,如果workInProgressRootRenderLanes不为空,那么则直接返回这个正在执行的任务的lane,当前新的任务则会和现有的任务进行一次批量更新:
if (
!deferRenderPhaseUpdateToNextBatch &&
(executionContext & RenderContext) !== NoContext &&
workInProgressRootRenderLanes !== NoLanes
) {
return pickArbitraryLane(workInProgressRootRenderLanes);
}
如果上面都不是,则会判断当前事件是否是过渡优先级,如果是,则会分配过渡优先级中的一个位置。
过渡优先级分配规则是:分配优先级时,会从过渡优先级的最右边开始分配,后续产生的任务则会依次向左移动一位,直到最后一个位置被分配后,后面的任务会从最右边第一个位置再开始做分配:
当前产生了一个任务A,那么会分配过渡优先级的最右边第一个位置:
TransitionLane1 = 0b0000000000000000000000001000000
现在又产生了任务B,那么则从A的位置向左移动一位:
TransitionLane2 = 0b0000000000000000000000010000000
后续产生的任务则会依次向左移动一位,过渡优先级共有16位:
TransitionLanes = 0b0000000001111111111111111000000
当最左边的1的位置被分配后,则又从最右边第一位1的位置开始赋予事件过渡优先级。
如果不是过渡优先级的任务,则接着往下找,可以看到接下来调用了getCurrentUpdatePriority函数,记得我们最开始讲到过,当项目初次渲染的时候,会在root容器上做事件委托并将所有支持的事件做优先级分类,当事件触发时会调用setCurrentUpdatePriority函数设置当前事件的优先级。调用getCurrentUpdatePriority函数也就获取到了事件触发时设置的事件优先级。获取到的事件优先级不为空的话,则会直接返回该事件的优先级。
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
return updateLane;
}
如果上面都没有找到事件优先级,则是会调用getCurrentEventPriority来获取React的外部事件的优先级,比如:在setTimeout中调用了setState方法:
const eventLane: Lane = (getCurrentEventPriority(): any);
return eventLane;
最后将找到的事件的优先级返回。
使用事件的优先级
现在我们已经看到是如果获取到事件的优先级了,那么是如果使用Lane的呢?我们接下来看。
首先会创建一个更新对象,将事件的lane添加到更新对象上:
const update = createUpdate(eventTime, lane);
export function createUpdate(eventTime: number, lane: Lane): Update<*> {
const update: Update<*> = {
eventTime, //更新事件触发的时间
lane, // 事件更新的优先级
tag: UpdateState, // 类型:更新,替换,强制更新等
payload: null, // 需要更新的内容
callback: null, // 更新回调 setState的第二个参数
next: null, // 下一个更新对象
};
return update;
}
接着将需要更新的内容挂载到payload上,将更新回调函数挂载到更新对象的callback属性上:
update.payload = payload;
if (callback !== undefined && callback !== null) {
update.callback = callback;
}
然后将更新对象添加到当前组件对应的fiber节点上的更新队列中:
enqueueUpdate(fiber, update, lane);
更新队列是一个循环链表结构。
接着会调用scheduleUpdateOnFiber,做好调度任务前的准备,我们主要看其中几个重要的地方:
export function scheduleUpdateOnFiber(
fiber: Fiber,
lane: Lane,
eventTime: number,
): FiberRoot | null {
// 检查是否做了无限循环更新,比如:在render函数中调用了setState,如果是则会报错提示
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);
...
ensureRootIsScheduled(root, eventTime);
...
return root;
}
我们看到函数内部调用了markUpdateLaneFromFiberToRoot,这个函数主要的作用是更新当前fiber节点的lannes,表示当前节点需要更新,然后收集需要更新的子节点的lane,存放在父fiber上的childLanes属性上。在后面做更新时,会根据fiber节点上lannes*判断当前fiber节点是否需要更新,根据childLanes判断当前fiber的子节点是否需要更新。我们来看一下markUpdateLaneFromFiberToRoot内部是如何实现的:
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节点向上遍历直到根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);
}
node = parent;
parent = parent.return;
}
sourceFiber.return的意思是获取当前fiber节点的父级,后面也会出一章关于fiber的详解,这边就简单的提一下。
遍历更新完成后,则会返回fiber root节点。
markUpdateLaneFromFiberToRoot执行完毕,紧接着调用了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位的二进制。
假设updateLane为:0b000100 那么它在eventTimes中则是这种形式的:
[-1, -1, 44573.3452, -1, -1...]
markRootUpdated调用完成后,紧接着调用了ensureRootIsScheduled函数,准备开始任务的调度。
ensureRootIsScheduled是一个比较重要的函数,里面存在了高优先级任务插队和任务饥饿问题,以及批量更新的处理。那么我们来看一下该函数中是如何处理这些问题的。
任务饥饿问题
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
...
// 为当前任务根据优先级添加过期时间
// 并检查未执行的任务中是否有任务过期,有任务过期则expiredLanes中添加该任务的lane
// 在后续任务执行中以同步模式执行,避免饥饿问题
markStarvedLanesAsExpired(root, currentTime);
...
}
关于任务饥饿问题的处理,主要逻辑在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上,
// 如果存在的话,则会将该任务以同步模式去执行,避免任务饥饿问题
// ps: 什么饥饿问题?
// 饥饿问题是指当执行一个任务时,不断的插入多个比该任务优先级高的任务,那么
// 这个任务会一直得不到执行
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;
// 获取当前位置上任务的过期时间,如果没有则会根据任务的优先级创建一个过期时间
// 如果有则会判断任务是否过期,过期了则会将当前任务的lane添加到expiredLanes上
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
// 例如:
// lane = 16 = 10000
// ~lane = 01111
// lanes = 28 = 11100
// lanes = 12 = 01100 = lanes & ~lane
lanes &= ~lane;
}
}
可以看到主要逻辑是在循环处理pendingLanes。
首先会调用pickArbitraryLaneIndex函数获取pendingLanes中最左边1的位置,例如:
lanes = 28 = 0b0000000000000000000000000011100
然后使用了:
27 = Math.clz32(lanes);
获取到了最左边1前面所有0的个数,然后计算出最左边1的位置:
const index = 31 - 27; // 4
然后使用index获取到相对应expirationTimes中的过期时间,如果过期时间为空则会根据当前优先级生成一个过期时间,优先级越高过期时间越小。然后将过期时间添加到相应的位置。
expirationTimes与eventTimes一样也是31位长度的Array,对应Lane使用31位的二进制。
expirationTimes[index] = computeExpirationTime(lane, currentTime);
如果当前位置有过期时间,则会检查是否过期,如果过期则将当前lane添加到expiredLanes上,在后续执行该任务的时候使用同步渲染,避免任务饥饿的问题。
if (expirationTime <= currentTime) {
root.expiredLanes |= lane;
}
接着会将当前计算完成的lane从lanes中删除,每次循环删除一个,直到lanes等于0:
lanes &= ~lane;
任务插队
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;
}
处理完饥饿问题,接下来调用了getNextLanes获取到所有任务中优先级最高的任务的lane,我们来看下其中的源码:
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
const pendingLanes = root.pendingLanes;
//没有剩余任务的时候,跳出更新
if (pendingLanes === NoLanes) {
return NoLanes;
}
...
}
首先判断pendingLanes是否为空。根据之前的代码,每个产生的任务都会将它们各自的优先级添加到fiber root的pendingLanes的属性上,也就是说pendingLanes上保存了所有将要执行的任务的lane,如果pendingLanes为空,那么则表示任务全部执行完成,也就不需要更新了,直接跳出。
可以看到ensureRootIsScheduled中对于getNextLanes返回空的处理:
// 如果nextLanes为空则表示没有任务需要执行,则直接中断更新
if (nextLanes === NoLanes) {
// existingCallbackNode不为空表示有任务使用了concurrent模式被scheduler调用,但是还未执行
// nextLanes为空了则表示没有任务了,就算这个任务执行了但是也做不了任何更新,所以需要取消掉
if (existingCallbackNode !== null) {
// 使用cancelCallback会将任务的callback置为null
// 在scheduler循环taskQueue时,会检查当前task的callback是否为null
// 为null则从taskQueue中删除,不会执行
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
回过头继续看getNextLanes中的代码:
// 在将要处理的任务中检查是否有未闲置的任务,如果有的话则需要先执行未闲置的任务,不能执行挂起任务
// 例如:
// 当前pendingLanes为: 17 = 0b0000000000000000000000000010001
// NonIdleLanes = 0b0001111111111111111111111111111
// 结果为: = 0b0000000000000000000000000010001 = 17
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
//检查是或否还有未闲置且将要执行的任务
if (nonIdlePendingLanes !== NoLanes) {
//检查未闲置的任务中除去挂起的任务,是否还有未被阻塞的的任务,有的话则需要
//从这些未被阻塞的任务中找出任务优先级最高的去执行
// & ~suspendedLanes 相当于从 nonIdlePendingLanes 中删除 suspendedLanes
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
if (nonIdleUnblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
} else {
// nonIdleUnblockedLanes(未闲置且未阻塞的任务)是未闲置任务中除去挂起的任务剩下来的
// 如果nonIdleUnblockedLanes为空,那么则从剩下的,也就是挂起的任务中找到优先级最高的来执行
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
}
}
} else {
// The only remaining work is Idle.
// 剩下的任务都是闲置的
// 找出未被阻塞的任务,然后从中找出优先级最高的执行
const unblockedLanes = pendingLanes & ~suspendedLanes;
if (unblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(unblockedLanes);
} else {
// 进入到这里,表示目前的任务中已经没有了未被阻塞的任务
// 需要从挂起的任务中找出任务优先级最高的执行
if (pingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(pingedLanes);
}
}
}
如果pengdingLanes不为空,那么则会从pengdingLanes中取出未闲置将要处理的lanes,例如:
当前pendingLanes为: = 0b0100000000000000000000000010001
最左边的1的位置为闲置位置,代表了闲置任务,闲置任务优先级最低,需要处理完所有其它优先级的任务后,再处理闲置任务。
NonIdleLanes = 0b0001111111111111111111111111111
NonIdleLanes表示了所有未闲置的1的位置,使用&符号运算(同位比较,值都为1,则结果为1,否则为0),取出未闲置的任务:
结果为: = 0b0000000000000000000000000010001 = 17
如果有未闲置的lanes,那么会优先找到未闲置lanes中未被阻塞的lane,如果没找到,则会从挂起的lanes中找到优先级最高的lane。
如果没有未闲置的lanes,则会从闲置的lanes中优先找未被阻塞的lane,如果没找到,则从闲置lanes中找到所有挂起的lanes,从中找出优先级最高的lane。
以上都没找到的话,则会返回一个空,跳出更新:
//从pendingLanes中找不到有任务,则返回一个空
if (nextLanes === NoLanes) {
return NoLanes;
}
如果当前又任务正在执行,那么wipLanes就不等于空,需要将新的任务的优先级与正在执行的任务的优先级进行比较。如果新任务比正在执行的任务的优先级低,那么则不会去管它,继续渲染,反之,新任务的优先级比正在执行的任务高,那么则取消当前任务,先执行新任务:
// wipLanes是正在执行任务的lane,nextLanes是本次需要执行的任务的lane
// wipLanes !== NoLanes:wipLanes不为空,表示有任务正在执行
// 如果正在渲染,突然新添加了一个任务,但是这个新任务比正在执行的任务的优先级低,那么则不会去管它,继续渲染
// 如果新任务的优先级比正在执行的任务高,那么则取消当前任务,执行新任务
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;
}
}
我们再看ensureRootIsScheduled中是如何处理的:
const existingCallbackNode = root.callbackNode;
...
const newCallbackPriority = getHighestPriorityLane(nextLanes);
const existingCallbackPriority = root.callbackPriority;
// nextLanes获取的是所有任务中优先级最高任务的lane
// 那么与当前现有的任务的优先级比较,只会有两种结果:
// 1.与现有的任务优先级一样,那么则会中断当前新任务向下的执行,重用之前现有的任务
// 2.新任务的优先级大于现有的任务优先级,那么则会取消现有的任务的执行,优先执行优先级高的任务
// 与现有的任务优先级一样的情况
if (
existingCallbackPriority === newCallbackPriority
) {
return;
}
// 新任务的优先级大于现有的任务优先级
// 取消现有的任务的执行
if (existingCallbackNode != null) {
cancelCallback(existingCallbackNode);
}
现在获取到了所有任务中优先级最高的lane,和现有任务的优先级existingCallbackPriority,那么nextLanes与当前现有的任务的优先级比较,只会有两种结果:
- 与现有的任务优先级一样,那么则会中断当前新任务向下的执行,重用之前现有的任务
- 新任务的优先级大于现有的任务优先级,那么则会取消现有的任务的执行,优先执行优先级高的任务,实现高优先级任务插队
任务调度开始
接下来则是开始创建任务,执行任务调度:
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
...
// 开始调度任务
// 判断新任务的优先级是否是同步优先级
// 是则使用同步渲染模式,否则使用并发渲染模式(时间分片)
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
...
newCallbackNode = null;
} else {
let schedulerPriorityLevel;
// lanesToEventPriority函数将lane的优先级转换为React事件的优先级,然后再根据React事件的优先级转换为Scheduler的优先级
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;
}
//将react与scheduler连接,将react产生的事件作为任务使用scheduler调度
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
判断新任务的优先级是否是同步优先级,是则使用同步渲染模式,否则使用并发渲染模式使用scheduler调度任务, 在使用并发模式时,会将lane的优先级转换为React事件的优先级,然后再根据React事件的优先级转换为Scheduler的优先级,Scheduler会根据它自己的优先级给任务做时间分片。
由于篇幅太长,我准备把接下来更新部分lanes的使用写到React源码解析之优先级Lane模型下中,笔芯...