React@17 引入了lane 车道 模型来代替之前的expirationTime过期时间的方式来管理调度优先级。
早期版本的React,每次更新都会创建一个Update对象,这个对象包含一个过期时间 expirationTime
过期时间越小,优先级越高,和我们前面说的scheduler库处理逻辑是一样的。
这样会带来一些问题
1. expirationTime无法很好的表示批次的概念,判断某些更新Update是否处于一个批次的判断方式是,判断当前Update的过期时间是否在当前批次处理时间范围之内,这样带来的问题是,批次必须是连续的, 1 2 3 4 5 我不能吧 23 4 单独分成一批进行处理
2. 由于update的执行必须按照过期时间的顺序来,比如 2 3 不执行无法执行 4 ,这样就导致,如果2/3是个IO阻塞任务,比如Suspense,就会造成4的阻塞 4可能是个CPU密集任务,但是在等待的过程中,造成CPU资源浪费.
什么是Lane
lane即车道的意思,其本质是个二进制的数字类型,其定义如下: (fiberLanes.ts)
/** 单车道 */
export type Lane = number;
/** 多车道 (优先级合集) */
export type Lanes = number;
export const TotalLanes = 31;
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
export const SyncUpdateLanes: Lane =
SyncLane | InputContinuousLane | DefaultLane;
export const TransitionLane: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111110000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000010000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane3: Lane = /* */ 0b0000000000000000000001000000000;
const TransitionLane4: Lane = /* */ 0b0000000000000000000010000000000;
const TransitionLane5: Lane = /* */ 0b0000000000000000000100000000000;
const TransitionLane6: Lane = /* */ 0b0000000000000000001000000000000;
const TransitionLane7: Lane = /* */ 0b0000000000000000010000000000000;
const TransitionLane8: Lane = /* */ 0b0000000000000000100000000000000;
const TransitionLane9: Lane = /* */ 0b0000000000000001000000000000000;
const TransitionLane10: Lane = /* */ 0b0000000000000010000000000000000;
const TransitionLane11: Lane = /* */ 0b0000000000000100000000000000000;
const TransitionLane12: Lane = /* */ 0b0000000000001000000000000000000;
const TransitionLane13: Lane = /* */ 0b0000000000010000000000000000000;
const TransitionLane14: Lane = /* */ 0b0000000000100000000000000000000;
const TransitionLane15: Lane = /* */ 0b0000000001000000000000000000000;
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
const RetryLane1: Lane = /* */ 0b0000000010000000000000000000000;
const RetryLane2: Lane = /* */ 0b0000000100000000000000000000000;
const RetryLane3: Lane = /* */ 0b0000001000000000000000000000000;
const RetryLane4: Lane = /* */ 0b0000010000000000000000000000000;
export const SomeRetryLane: Lane = RetryLane1;
export const SelectiveHydrationLane: Lane = /* */ 0b0000100000000000000000000000000;
const NonIdleLanes: Lanes = /* */ 0b0000111111111111111111111111111;
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;
可以看到,单个lane只能有一位是1,其余都是0,每个lane优先级1的位置都不同,互相没有重叠,数字越小,即1越靠前,优先级越高。
多个lane的或运算代表批的概念 即lanes,如
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111110000000;
表示所有的Transition优先级所占用的lane的合集
优先级从上到下 依次是同步优先级
SyncLane 同步优先级
InputContinuousLane 连续输入事件优先级
DefaultLane 默认优先级
TransitionLane 过度优先级
IdleLane 等待优先级 等等
Lane的基本运算
合并Lane
使用按位或的方式即可
/**
* 合并两个优先级
* @param lane1
* @param lane2
* @returns mergedLanes
*/
export function mergeLane(lane1: Lane, lane2: Lane): Lanes {
return lane1 | lane2;
}
从lanes中移除某个lane
使用lanes & ~lane 的方式,如下:
/**
* 从某个lanes集合中 移除单个lane/lanes
* @param laneSet
* @param subset
* @returns
*/
export function removeLanes(laneSet: Lanes, subset: Lane | Lanes): Lanes {
return laneSet & ~subset;
}
获取最高优先级lane
注意,lane模型中,数字越小,优先级越高,基于这个设定,可以通过 lanes&-lanes 获取最高优先级的lane
/**
* 从某个lanes集合中,找到优先级最高的lane
* 注意,lane模型中,越小优先级越高,计算对应如下
* ( min(lanes) = lanes&-lanes 注意-lanes 是二进制的取反+1)
* 举例: 0b00110 => 取反 0b11001 => 加1 => 0b11010 => 0b00110 & 0b11010 = 0b00010
* @param lanes
* @returns
*/
export function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes;
}
判断lanes集合是不是另外一个lanes集合的子集
使用 lanes & subLanes === subLanes 来判断,如下:
/**
* 判断某个lane集合是不是另外一个lanes集合的子集 sub必须完全包含在
* @param laneSet
* @param subSet
*/
export function isSubsetOfLanes(laneSet: Lanes, subSet: Lanes | Lane) {
return (laneSet & subSet) === subSet;
}
判断一个lanes集合是否和另一个lanes有交集
使用 lanes1 & lane2 === NoLane
/**
* 某个set集合 是否包含某个集合的部分lane 包含即可
* @param laneSet
* @param subSet
* @returns
*/
export function includeSomeLanes(laneSet: Lanes, subSet: Lanes | Lanes) {
return (laneSet & subSet) !== NoLanes;
}
使用如上运算,我们就可以对优先级进行合并,移除,或者是判断一批更新中是否包含了某些更新。
lane结合scheduler
我们知道,scheduler单独发包,采用expirationTime的方式,处理优先级。
而react中,引用了scheduler,如果优先级是 SyncLane 则把更新任务放到微任务队列执行,如果低于SyncLane 则放到scheduler中转换成expirationTime运行。
react中,实现了lanes和cheduler优先级互相转换的函数:
/**
* lanes优先级转scheduler优先级 (取lanes中优先级最高的lane 调用getHighestPriorityLane)
* @param lanes
*/
export function lanesToSchedulerPriority(lanes: Lanes): PriorityLevel {
const highestPriorityLane = getHighestPriorityLane(lanes);
switch (highestPriorityLane) {
case SyncLane:
return PriorityLevel.IMMEDIATE_PRIORITY;
case InputContinuousLane:
return PriorityLevel.USER_BLOCKING_PRIORITY;
case TransitionLane:
return PriorityLevel.LOW_PRIORITY;
default:
return PriorityLevel.NORMAL_PRIORITY;
}
}
/** 转换函数 */
/**
* scheduler 优先级 转 lane优先级
* @param schdulerPriority
*/
export function schedulerPriorityToLane(schdulerPriority: PriorityLevel): Lane {
switch (schdulerPriority) {
case PriorityLevel.IMMEDIATE_PRIORITY:
return SyncLane;
case PriorityLevel.USER_BLOCKING_PRIORITY:
return InputContinuousLane;
case PriorityLevel.NORMAL_PRIORITY:
return DefaultLane;
default:
return IdleLane;
}
}
如何获得优先级lane
当react中触发一个更新时,需要给当前update分配一个优先级lane,其实现方法就是
requestUpdateLane 实现如下:
/**
* 根据当前update触发上下文 获取update优先级
* 比如 setState在effect中触发和在onclick中触发 有不一样的优先级
*/
export function requestUpdateLane(): Lane {
if (isTransition) {
return TransitionLane
}
const currentUpdateLane = schedulerPriorityToLane(
scheduler.getCurrentPriorityLevel()
);
return currentUpdateLane;
}
这里需要详细说一下,React通过scheduler.runWithPriority 和 getcurrentPriority完成和scheduler之间的优先级获取和设置。
React源码揭秘 | scheduler 并发更新原理_react并发更新如何实现任务切片的-优快云博客
这篇文章详细讲了scheduler的工作原理,这里不赘述。
在updateContainer时,创建update并且入队,这部分的逻辑被放到了scheduler.runWithPrioriy中运行
const updateContainer = (element: ReactElement, root: FiberRootNode) => {
// 默认情况下 同步渲染
scheduler.runWithPriority(PriorityLevel.IMMEDIATE_PRIORITY, () => {
// 请求获得当前更新lane
const lane = requestUpdateLane();
// 获hostRootFiber
const hostRootFiber = root.current;
// 更新的Element元素入队
hostRootFiber.updateQueue?.enqueue(
new Update<ReactElement>(element, lane),
hostRootFiber,
lane
);
// scheduleUpdateOnFiber 调度更新
scheduleUpdateOnFiber(root.current, lane);
});
};
runWithPriority会立刻运行传入的函数,并且在上下文把全局优先级设置为传入的优先级。
在传入的函数中,通过requestUpdateLane获取优先级,其实就是调用getCurrentPriority
这个函数会把runQWithPriority设置的全局优先级返回,再通过schedulerPriorityToLane转换成lane优先级,即SyncLane.
可以看到,react默认情况下,都是以Sync同步的方式运行更新的
函数执行之后,runWithPriority会讲全局优先级改回之前的优先级(默认的 NORMAL_PRIORITY)
runWithPriority实现如下:
/** 以某优先级同步运行 */
runWithPriority(priorityLevel: PriorityLevel, callback: any) {
const priviouseLevel = this.currentPriorityLevel
this.currentPriorityLevel = priorityLevel
try {
callback()
} catch (e) {
console.warn(e)
} finally {
this.currentPriorityLevel = priviouseLevel
}
}
其流程如图所示:
合成事件中优先级处理
对于不同的事件有不同的优先级 比如用户点击事件 用户拖拽事件 等等
react实现了一个函数,用来把事件类型转换成scheduler优先级 如下:
/** 事件转优先级 */
export function eventTypeToSchedulerPriority(eventType: string) {
switch (eventType) {
case "click":
case "keydown":
case "keyup":
case "keydown":
case "keypress":
case "keyup":
case "focusin":
case "focusout":
return PriorityLevel.IMMEDIATE_PRIORITY;
case "scroll":
case "resize":
case "mousemove":
case "mouseenter":
case "mouseleave":
case "touchstart":
case "touchmove":
case "touchend":
return PriorityLevel.USER_BLOCKING_PRIORITY;
case "input":
case "change":
case "submit":
case "focus":
case "blur":
case "select":
case "drag":
case "drop":
case "pause":
case "play":
case "waiting":
case "ended":
case "canplay":
case "canplaythrough":
return PriorityLevel.NORMAL_PRIORITY;
case "abort":
case "load":
case "loadeddata":
case "loadedmetadata":
case "error":
case "durationchange":
return PriorityLevel.LOW_PRIORITY;
default:
return PriorityLevel.IDLE_PRIORITY;
}
}
在合成事件触发中,会调用这个函数,获取当前合成事件的scheduler优先级,并且调用runWithPriority 如下:
/* 执行事件 */
function triggerEventListeners(
listeners: SyntheticEventListener[],
event: Event
) {
listeners.forEach((listener) =>
scheduler.runWithPriority(eventTypeToSchedulerPriority(event.type), () => {
listener(event);
})
);
}
当我们在不同的合成事件中触发更新时,比如setState 或者 hooks的setter函数,其内部就会创建一个更新Update,并且调用 requestUpdateLane获得当前scheduler的全局优先级,并且转换成lane。通过这种方式,在不同优先级的上下文中触发更新,都会获得当前上下文对应的优先级。
优先级调度&批处理
我们知道,React17版本之前,setState是异步的,也就是说你无法在设置完state之后立刻获得最新的状态值,然而你如果把setState放到setTimeout中脱离react的调度,则就是同步运行了。
在新版本的React中,由于lanes机制的引入,任何地方的setState都是异步运行了,为什么?
早起React采用类scheduler的expirationTime过期时间来完成优先级调度,这就导致一次批量更新必须更新过期时间连续的Update,React中包含一个变量来记录当前批处理的范围,也就是判读当前更新的过期时间是否在当前批处理的范围内。
这就导致,对于在批处理范围内的更新,React会调度异步处理,但是对于setttimeout内部的setState,由于其执行在同步代码之后,当运行的时候批处理过程已经结束了,在老版本React的ensureRootIsScheduled中会检查,如果当前更新已经在批处理之后了,就会同步的调用处理。
新版本的React由于采用lane模型,这就使不论settimeout还是同步代码中的setState都统一的根据lane处理,同一批次只处理同一个lane的任务,这就和过期时间无关了,不论setState在什么时候执行,React在一次更新中只会处理当前更新lane对应的Uodate。
在详细介绍更新流程之前,我们先补充一些变量和属性
wipRenderLane - 当前workLoop正在处理的lane
在workLoop.ts中,定义一个全局变量wipRenderLane: Lane = NoLane
/** 表示当前正在render阶段对应的任务对应的lane 用来在任务中断后重启判断跳过初始化流程 */
let wipRootRenderLane: Lane = NoLane;
其是一个lane类型,表示当前更新所对应的处理优先级Lane
FiberRootNode.pendingLanes & finishedLane
在全局root节点上,定义
1. pendingLanes,表示当前react等待处理的lanes
对应两个操作函数:
/**
* 把某个更新的lane加入到root.pendingLanes
* @param root
* @param lane
*/
export function markRootUpdated(root: FiberRootNode, lane: Lane) {
root.pendingLanes = mergeLane(root.pendingLanes, lane);
}
/**
* 去掉root的某条lane (标记lane对应任务执行完成)
* @param root
* @param lane
*/
export function markRootFinished(root: FiberRootNode, lane: Lane) {
root.pendingLanes = removeLanes(root.pendingLanes, lane);
}
2. finishedLane 表示在render阶段处理完的lane,给commit阶段用 类似于finishedWork
lane冒泡
当一个更新触发时,其内部在把更新enqueue进updateQueue之后,会调用scheduleUpdateOnFib
er函数来触发更新渲染
/** 派发修改state */
function dispatchSetState<State>(
fiber: FiberNode,
updateQueue: UpdateQueue<State>,
action: Action<State>
) {
// 获取一个优先级 根据 dispatchSetState 执行所在的上下文
const lane = requestUpdateLane();
// 创建一个update对象
const update = new Update(action, lane);
...... ... ...
// 入队 并且加入到fiber上
updateQueue.enqueue(update, fiber, lane);
// 开启调度时,也需要传入当前优先级
scheduleUpdateOnFiber(fiber, lane);
}
在scheduleUpdateOnFiber中,会先调用markUpdateFromFiberToRoot,将当前更新的lane,冒泡到其祖先节点的childLanes上,并且调用markRootUpdate把当前更新lane合并加入到root.pendingLanes上,实现如下:
/** 在Fiber中调度更新 */
export function scheduleUpdateOnFiber(fiberNode: FiberNode, lane: Lane) {
/** 先从更新的fiber节点递归到hostRootFiber
* 这个过程中,一个目的是寻找fiberRootNode节点
* 一个是更新沿途的 childLines
*/
const fiberRootNode = markUpdateLaneFromFiberToRoot(fiberNode, lane);
// 更新root的pendingLane, 更新root节点的pendingLanes 表示当前正在处理的lanes
markRootUpdated(fiberRootNode, lane);
// 保证根节点被正确调度
ensureRootIsScheduled(fiberRootNode);
}
ensureRootIsSchuduled函数用来调度更新任务,其实现如下:
export function ensureRootIsScheduled(root: FiberRootNode) {
// 先实现同步调度 获取当前最高优先级
const highestPriorityLane = getNextLane(root);
// 判断,如果不存在优先级 说明没有任务需要继续调度了 直接returna
if (highestPriorityLane === NoLane) return;
// 批处理更新, 微任务调用更新
if (highestPriorityLane === SyncLane) {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
// 设置微任务回调 冲洗缓冲区
flushSyncCallbacks();
} else {
// 其他优先级 使用scheduler调度
scheduler.scheduleCallback(
lanesToSchedulerPriority(highestPriorityLane),
performConcurrentWorkOnRoot.bind(null, root)
);
}
}
可以看到,此函数先从root.pendingLanes上获取一个优先级最高的lane,即getNextLane, 其内部就是对pendingLanes调用getHighestPriorityLane函数
/**
* 获取当前root优先级最高的lan
* @param lanes
*/
export function getNextLane(root: FiberRootNode): Lane {
const pendingLanes = root.pendingLanes;
/** 调用getHighestPriorityLane 获取最高优先级lane */
return getHighestPriorityLane(pendingLanes);
}
获取优先级之后检查,如果是NoLane 表示当前已经不存在更新了,直接return
如果是同步优先级 SyncLane
则需要同步处理更新,需要将更新逻辑放到微任务执行,实现如下:
/** 同步任务更新队列 */
type SyncCallback = (...args: any[]) => void;
/** 存储callback数组 */
let syncTaskQueue: SyncCallback[] = [];
/** 入队 */
export function scheduleSyncCallback(callback: SyncCallback) {
syncTaskQueue.push(callback);
}
可以看到 scheduleSyncCallback本质就是把performWorkOnRoot 绑定root之后,推入一个数组等待运行
推入数组之后,调用flushSyncCallback启动微任务调度,其本质上就是把callback处理函数通过queueMicroTask排入微队列等待运行,只不过对不同环境进行了兼容,如下:
function _flushSyncCallbacks() {
if (!isFlushingSyncQueue && syncTaskQueue.length > 0) {
// 上锁
isFlushingSyncQueue = true;
try {
// 执行任务
syncTaskQueue.forEach((syncTask) => scheduleMicroTask(syncTask));
} catch (e) {
console.error("同步微任务队列执行错误,错误信息:", e);
} finally {
// 执行结束 释放锁 清空队列
isFlushingSyncQueue = false;
syncTaskQueue = [];
}
}
}
/** 兼容多种环境的microTask */
export const scheduleMicroTask = (
typeof queueMicrotask === "function"
? queueMicrotask
: typeof Promise === "function"
? (callback: SyncCallback) => Promise.resolve().then(callback)
: setTimeout
) as (callback: SyncCallback) => void;
/** 微任务flush清空队列 */
export const flushSyncCallbacks = () => scheduleMicroTask(_flushSyncCallbacks);
_flushSyncCallback 函数会作为微任务执行,遍历数组内的performWorkOnRoot函数运行,最后置空数组。
如果优先级低于SyncLane
对于低于SyncLane的优先级lane,通过laneToSchedulerPriority转换成scheduler优先级并且交给scheduler调度运行。
lane批处理
为什么lane可以实现优先级,同时保证在同步/异步代码中,都能保证异步执行的特性?
看一个例子
function Comp(){
const [variableA,setVariableA] = useState<number>(0)
return <Button onClick={e=>{
setVariableA(100)
console.log(variableA)
setVariableA(200)
console.log(variableA)
}}></Button>
}
这个例子中,在点击按钮之后,react只会重新渲染一次,并且两次console输出,都是0
其原理是,在两次setVariableA时,setter函数会调用diapatchSetState函数,如下:
/** 派发修改state */
function dispatchSetState<State>(
fiber: FiberNode,
updateQueue: UpdateQueue<State>,
action: Action<State>
) {
// 获取一个优先级 根据 dispatchSetState 执行所在的上下文
const lane = requestUpdateLane();
// 创建一个update对象
const update = new Update(action, lane);
// 入队 并且加入到fiber上
updateQueue.enqueue(update, fiber, lane);
// 开启调度时,也需要传入当前优先级
scheduleUpdateOnFiber(fiber, lane);
}
可以看到
1. 这个函数首先获取当前优先级,由于是在onClick上下文调用的,所以其优先级为SyncLane,因为onClick是个离散事件,优先级最高!
2. 把action(第一次调用的值100)和lane传入Update构造函数创建update对象
3. 把update对象推入当前hook的updateQueue (注意 hook对象也有updataQueue这个后面细说)
4. 开启调度
在ensureRootIsScheduled函数中,拿到最高优先级的lane,也就是SyncLane,并且将此次更新需要执行的performSyncWorkOnRoot函数推入微队列,等到同步任务运行结束后运行。
继续执行onClick函数,此时获取variableA,由于更新还没进行,此时variableA还是0
执行到第二个setter,同上面逻辑,再次获取variableA 依旧是0 因为更新还没进行
- 此时的微任务队列包含两个更新任务,即两次performWorkOnRoot的执行
- 此时的variableA对应的hooks的updateQueue包含两个update更新
当onClick函数结束,此时执行微任务。
performSyncWorkOnRoot逻辑如下:
/** 从root开始 处理同步任务 */
export function performSyncWorkOnRoot(root: FiberRootNode) {
// 获取当前的优先级
const lane = getNextLane(root);
if (lane !== SyncLane) {
/**
* 这里 lane如果不是同步任务了,说明同步任务的lane已经被remove 应该执行低优先级的任务了
* 此时应该停止执行当前任务 重新调度
* 【实现同步任务的批处理,当第一次执行完之后 commit阶段remove SyncLane 这里就继续不下去了,
* 后面微任务中的 performSyncWorkOnRoot都不执行了】
*/
return ensureRootIsScheduled(root);
}
// 开始生成fiber 关闭并发模式
const exitStatus = renderRoot(root, lane, false);
switch (exitStatus) {
// 注意 同步任务一次性执行完 不存在RootInComplete中断的情况
case RootCompleted:
// 执行成功 设置finishedWork 和 finishedLane 并且commit
// 设置root.finishedWork
root.finishedWork = root.current.alternate;
root.finishedLane = lane;
// 设置wipRootRenderLane = NoLane;
wipRootRenderLane = NoLane;
commitRoot(root);
default:
// TODO Suspense的情况
}
}
第一个performSyncWorkOnRoot执行,运行到useState的时候,其内部逻辑会一次性把当前hook上updateQueue中的两个update都执行完,此时的variableA已经是200了
第一次update执行之后,commit阶段,会调用markRootFInished,把当前执行的Synclane,从root.pendingLanes中去除,此时pendingLanes上为空 即NoLanes
执行第二个performSynWorkOnRoot,此时getNextLane得到NoLane,已经没有任务需要运行了,此时lane !== SyncLane 函数结束运行,第二次更新终止。
这样就达到了批处理任务的效果,不论setVaribaleA多少次,react都会在第一次更新的时候处理完所有的update! 后面的更新都会忽略!
如果是异步任务,也都一样,可以对照代码走一遍异步任务的情况!
那么为什么settimeout中也是异步的,很简单,因为setVaribleA执行的时候 只是向hook.updateQueue中推入了一个更新对象,并没有真正的更新,所以不论在哪里运行setter 都是异步。(区分老版本根据过期时间判断)
总结一下,为什么lane能更好的实现批处理,你可以把lane想象成一个可以被消费的数据,在setState的时候,会在pendingLanes设置lane,但是相同优先级的多个setState只能设置一个lane,那么在更新(消费)的过程中,当第一个performWorkOnRoot消费完第一个lane之后,后面的更新获取不到lane就会结束运行,避免多次render。
同时 lane的每一项对独立,可以自由的决定先调度哪些批次的优先级更新,不用像expirationTime一样,必须按顺序调度!