这几天看到Scheduler最近新增了Task的类型,也就是要添加到队列的任务;其中的callback属性,是实际要执行的更新等操作;这里提个小问题,callback既然是要执行的任务回调,什么时候会是null呢?
重学React18(一):render函数中写到了scheduleUpdateOnFiber,这一篇就来看下react的任务调度,会找到答案~
scheduleUpdateOnFiber
scheduleUpdateOnFiber(packages\react-reconciler\src\ReactFiberWorkLoop.old.js)是react18每一次更新都会调用的方法,为方便理解,对源码做一些简化,先看下主要过程:
export function (
root: FiberRoot,
fiber: Fiber,
lane: Lane,
eventTime: number,
) {
// * 省略掉一些逻辑判断,初次渲染不会走到,更新时才会走到
// ...scheduleUpdateOnFiber
// Mark that the root has a pending update.
markRootUpdated(root, lane, eventTime); // * 将lane合并到root.pendingLanes上。root.pendingLanes |= lane; suspendedLanes, pingedLanes设置为NoLanes,后面getNextLanes会用到
// ...
ensureRootIsScheduled(root, eventTime);
// ...
}
其中ensureRootIsScheduled是更新的必经之路,负责不同优先级任务的调度,并产生调度优先级
// Use this function to schedule a task for a root. There's only one task per
// root; if a task was already scheduled, we'll check to make sure the priority
// of the existing task is the same as the priority of the next level that the
// root has work on. This function is called on every update, and right before
// exiting a task.
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// Determine the next lanes to work on, and their priority.
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
// ...
// * 取出接下来要处理的最紧急任务lanes中的最高优先级
// We use the highest priority lane to represent the priority of the callback.
const newCallbackPriority = getHighestPriorityLane(nextLanes);
// * 跟当前任务优先级比较, 优先级相同则复用,直接return
// Check if there's an existing task. We may be able to reuse it.
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
// The priority hasn't changed. We can reuse the existing task. Exit.
return;
}
// * 优先级不同,则取消当前任务,`scheduleCallback`生成一个新的调度,并更新`root`
// * 初次渲染,root.callbackPriority为NoLane,即0,所以会直接调用scheduleCallback
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback(existingCallbackNode);
}
// Schedule a new callback.
let newCallbackNode;
// ...
// 如果本地调度优先级最高的lane是SyncLane,则进入【同步调度】
// scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); 将performSyncWorkOnRoot添加到syncQueue队列
// 然后利用scheduleMicrotask在微任务中遍历syncQueue并同步执行所有performSyncWorkOnRoot回调
// 下面是非同步过程
let schedulerPriorityLevel = // ...; // * 通过条件判断,将lanes转化成相应的调度优先级:ImmediateSchedulerPriority | UserBlockingSchedulerPriority | NormalSchedulerPriority | IdleSchedulerPriority;
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
通过函数命名和注释,可以知道ensureRootIsScheduled的过程:
- 取出下一个最高优先级的任务
- 跟当前任务优先级比较
- 优先级相同则复用,直接return
- 优先级不同,则取消当前任务,
scheduleCallback生成一个新的任务,并更新root
scheduleCallback
接下来,就要看下scheduleCallback和performConcurrentWorkOnRoot了,为避免篇幅过长,本文先看一下前者,梳理任务调度流程,后者放在下一篇。
scheduleCallback对应的是Scheduler.js中的unstable_scheduleCallback,
看这个方法之前,我们先看下Scheduler.js中定义的一些全局变量,
其中最主要的就是这两个队列
// Tasks are stored on a min heap
var taskQueue: Array<Task> = []; // 即将进行调度的任务的集合(不需要延时和延时时间到了的任务)
var timerQueue: Array<Task> = []; // 需要延时执行的任务的集合
React需要先处理优先级高的任务,所以taskQueue,timerQueue都采用了小顶堆的排序方式。
因为task中的排序字段sortIndex取的是expirationTime和startTime, 值越小,优先级越高。小顶堆每次取出堆顶元素,就是优先级最高的任务啦☺
关于堆的push和pop等操作都在packages\scheduler\src\SchedulerMinHeap.js文件中,可以去看一下这些方法的实现,加深对堆操作的理解。
function unstable_scheduleCallback(
priorityLevel: PriorityLevel,
callback: Callback,
options?: {delay: number},
): Task {
var currentTime = getCurrentTime();
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;
}
var timeout = // ...; // * 根据调度优先级priorityLevel设置相应的超时时间:NORMAL_PRIORITY_TIMEOUT | IMMEDIATE_PRIORITY_TIMEOUT | USER_BLOCKING_PRIORITY_TIMEOUT | LOW_PRIORITY_TIMEOUT | IDLE_PRIORITY_TIMEOUT
var expirationTime = startTime + timeout;
// * 这里可以看到Scheduler中的任务的结构了,其中callback属性,也就是被调度后执行的方法,是传入的performConcurrentWorkOnRoot或者performSyncWorkOnRoot
var newTask: Task = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (startTime > currentTime) { // * 有delay的情况
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask); // * 将当前任务添加到timerQueue,并按照sortIndex(startTime)重新排列小顶堆
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// * taskQueue为空,并且当前任务是可以最先执行的延时任务
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout. // * 取消定时器,定时器是由requestHostTimeout创建的
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime); // * 重新创建一个定时器
}
} else { // * 没有delay的情况
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
立即执行任务
先看下立即执行的任务的这个条件分支
- 任务的sortIndex设置为expirationTime
- 将任务添加到taskQueue,并且按照sortIndex,重新调整成小顶堆
- requestHostCallback(flushWork),即flushWork赋值给全局变量scheduledHostCallback,然后触发performWorkUntilDeadline
// * 根据环境,选择使用setImmediate、MessageChannel、setTimeout中的一个方法
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// Keep track of the start time so we can measure how long the main thread
// has been blocked.
startTime = currentTime;
const hasTimeRemaining = true;
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
//
// Intentionally not using a try-catch, since that makes some debugging
// techniques harder. Instead, if `scheduledHostCallback` errors, then
// `hasMoreWork` will remain true, and we'll continue the work loop.
let hasMoreWork = true;
try {
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
if (hasMoreWork) {
// If there's more work, schedule the next message event at the end
// of the preceding one.
port.postMessage(null);
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
可以看到上面的performWorkUntilDeadline调用flushWork, 还没看到flushWork的实现,不过我们可以猜测flushWork应该是不断执行taskQueue中的任务,直到没有TimeRemaining了,返回taskQueue中是否还有待执行的任务,等待下一次执行,是不是有requestIdleCallback内味儿啦~
延时任务
- 任务的sortIndex设置为startTime
- 将任务添加到timerQueue,并且按照sortIndex,重新调整成小顶堆
- 判断下taskQueue是否为空,当前任务是不是timerQueen中需要最先调度的任务
- 上面的条件成立的话,requestHostTimeout(handleTimeout, startTime - currentTime),即重新创建一个定时器,延时时间是delay,回调方法是handleTimeout
那么,主要逻辑就是handleTimeout了
function handleTimeout(currentTime: number) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime); // * 取出timerQueue中所有startTime<=currentTime的任务,放入taskQueue,准备接受调度
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) { // * taskQueue不为空,执行其中任务的callback
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else { // * taskQueue为空,检查下timeQueue,重复上面放入taskQueue的逻辑
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
以上,就是react18任务调度的整体流程,再来总结一下
- render阶段,创建了一个update对象,并挂载到fiber的UpdateQueue上,然后调用scheduleUpdateOnFiber
- 调用markRootUpdated,将update对象的lane合并到root.pendingLanes,表示有更新
- 调用ensureRootIsScheduled进行调度
- 从root.pendingLanes中取出接下来要处理的任务,和它们中的最高优先级
- 最高优先级,放在root.callbackPriority上,以便下次调度的时候做比较,看是否需要取消已有的任务,
- 将事件优先级转化成schedulerPriorityLevel
- 调用scheduleCallback,通过schedulerPriorityLevel和回调,创建一个task,并挂载到root.callbackNode, 表示当前已经有任务被调度了
- 通过taskQueue和timerQueue,实现立即执行任务和延时任务的执行
问题
callback是每个任务被调度的时候,执行的方法,就是函数组件的更新等,那既然被调度了,什么情况会是null呢(答案我们在上面的调度过程已经梳理过了,具体实现就在cancelCallback方法中哦~)- 浏览器环境下,requestIdleCallback改成MessageChannel的好处是什么
- 新旧任务的优先级不同,会取消旧的任务,新任务的优先级一定比旧任务的优先级高吗