React
运行时,如果把别的部分比喻成我们的肢体用来执行具体的动作,那么scheduler
就相当于我们的大脑,调度中心位于scheduler
包中,理解清楚scheduler
为我们理解react
的工作流程有很大的裨益。
我们都知道react
可以运行在node
环境中和浏览器环境中,所以在不同环境下实现requesHostCallback
等函数的时候采用了不同的方式,其中在node
环境下采用setTimeout
来实现任务的及时调用,浏览器环境下则使用MessageChannel
。这里引申出来一个问题,react
为什么放弃了requesIdleCallback
和setTimeout
而采用MessageChannel
来实现。这一点我们可以在这个PR[1]中看到一些端倪
1 . 由于requestIdleCallback
依赖于显示器的刷新频率,使用时需要看vsync cycle(指硬件设备的频率)
的脸色
2 . MessageChannel
方式也会有问题,会加剧和浏览器其它任务的竞争
3 . 为了尽可能每帧多执行任务,采用了5ms间隔的消息event
发起调度,也就是这里真正有必要使用postmessage
来传递消息
4 . 对于浏览器在后台运行时postmessage
和requestAnimationFrame
、setTimeout
的具体差异还不清楚,假设他们拥有同样的优先级,翻译不好见下面原文
I'm also not sure to what extent message events are throttled when the tab is backgrounded, relative to
requestAnimationFrame
orsetTimeout
. I'm starting with the assumption thatmessage
events fire with at least the same priority as timers, but I'll need to confirm.
由此我们可以看到实现方式并不是唯一的,可以猜想。react
团队做这一改动可能是react
团队更希望控制调度的频率,根据任务的优先级不同,提高任务的处理速度,放弃本身对于浏览器帧的依赖。优化react
的性能(concurrent
)
见MDN[2]
调度中心比较重要的函数在SchedulerHostConfig.default.js中
该js文件一共导出了8个函数
export let requestHostCallback;//请求及时回调
export let cancelHostCallback;
export let requestHostTimeout;
export let cancelHostTimeout;
export let shouldYieldToHost;
export let requestPaint;
export let getCurrentTime;
export let forceFrameRate;
请求或取消调度
requestHostCallback
详情见:源码[3]
cancelHostCallbac
详情见:源码[4]
requestHostTimeout
详情见:源码[5]
requestHostTimeout
详情见:源码[6]
这几个函数的代码量非常少,它们的作用就是用来通知消息请求调用或者注册异步任务等待调用。下面我们具体看下scheduler的整个流程
这个函数注册了一个任务并开始调度。
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
// 确定当前时间 startTime 和延迟更新时间 timeout
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;
}
// 根据优先级不同timeout不同,最终导致任务的过期时间不同,而任务的过期时间是用来排序的唯一条件
// 所以我们可以理解优先级最高的任务,过期时间越短,任务执行的靠前
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
var expirationTime = startTime + timeout;
var newTask = {
id: taskIdCounter++,
// 任务本体
callback,
// 任务优先级
priorityLevel,
// 任务开始的时间,表示任务何时才能执行
startTime,
// 任务的过期时间
expirationTime,
// 在小顶堆队列中排序的依据
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
// 如果是延迟任务则将 newTask 放入延迟调度队列(timerQueue)并执行 requestHostTimeout
// 如果是正常任务则将 newTask 放入正常调度队列(taskQueue)并执行 requestHostCallback
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
// 会把handleTimeout放到setTimeout里,在startTime - currentTime时间之后执行
// 待会再调度
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
// taskQueue是最小堆,而堆内又是根据sortIndex(也就是expirationTime)进行排序的。
// 可以保证优先级最高(expirationTime最小)的任务排在前面被优先处理。
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// 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;
}
开始调度任务,在这里我们可以看到scheduleHostCallback
这个变量被赋值成为了flushWork
见上段代码90行。
const channel = new MessageChannel();
const port = channel.port2;
// 收到消息之后调用performWorkUntilDeadline来处理
channel.port1.onmessage = performWorkUntilDeadline;
requestHostCallback = function(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
可以看到这个函数主要的逻辑设置deadline为当前时间加上5ms 对应前言提到的5ms,同时开始消费任务并判断是否还有新的任务以决定后续的逻辑
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// Yield after `yieldInterval` ms, regardless of where we are in the vsync
// cycle. This means there's always time remaining at the beginning of
// the message event.
// yieldInterval 5ms
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
// scheduledHostCallback 由requestHostCallback 赋值为flushWork
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there's more work, schedule the next message event at the end
// of the preceding one.
port.postMessage(null);
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
可以看到消费任务的主要逻辑是在workLoop
这个循环中实现的,我们在React
工作循环一文中有提到的任务调度循环。
function flushWork(hasTimeRemaining, initialTime) {
// 1. 做好全局标记, 表示现在已经进入调度阶段
isHostCallbackScheduled = false;
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
// 2. 循环消费队列
return workLoop(hasTimeRemaining, initialTime);
} finally {
// 3. 还原标记
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}}
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
// 获取taskQueue中最紧急的任务
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
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 = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// 检查callback的执行结果返回的是不是函数,如果返回的是函数,则将这个函数作为当前任务新的回调。
// concurrent模式下,callback是performConcurrentWorkOnRoot,其内部根据当前调度的任务
// 是否相同,来决定是否返回自身,如果相同,则说明还有任务没做完,返回自身,其作为新的callback
// 被放到当前的task上。while循环完成一次之后,检查shouldYieldToHost,如果需要让出执行权,
// 则中断循环,走到下方,判断currentTask不为null,返回true,说明还有任务,回到performWorkUntilDeadline
// 中,判断还有任务,继续port.postMessage(null),调用监听函数performWorkUntilDeadline,
// 继续执行任务
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
// return 的结果会作为 performWorkUntilDeadline 中hasMoreWork的依据
// 高优先级任务完成后,currentTask.callback为null,任务从taskQueue中删除,此时队列中还有低优先级任务,
// currentTask = peek(taskQueue) currentTask不为空,说明还有任务,继续postMessage执行workLoop,但它被取消过,导致currentTask.callback为null
// 所以会被删除,此时的taskQueue为空,低优先级的任务重新调度,加入taskQueue
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
解读:workLoop
本身是一个大循环,这个循环非常重要。此时实现了时间切片和fiber树的可中断渲染。首先我们明确一点task
本身采用最小堆根据sortIndex
也即expirationTime
。并通过
peek
方法从taskQueue
中取出来最紧急的任务。
每次while循环的退出就是一个时间切片,详细看下while
循环退出的条件,可以看到一共有两种方式可以退出
1 . 队列被清空:这种情况就是正常下情况。见49行从taskQueue
队列中获取下一个最紧急的任务来执行,如果这个任务为null
,则表示此任务队列被清空。退出workLoop
循环
2 . 任务执行超时:在执行任务的过程中由于任务本身过于复杂在执行task.callback之前就会判断是否超时(shouldYieldToHost
)。如果超时也需要退出循环交给performWorkUntilDeadline
发起下一次调度,与此同时浏览器可以有空闲执行别的任务。因为本身MessageChannel
监听事件是一个异步任务,故可以理解在浏览器执行完别的任务后会继续执行performWorkUntilDeadline
。
这段代码中还包含了十分重要的逻辑(见19~36行),这段代码是实现可中断渲染的关键。具体它们是怎么工作的呢以concurrent
模式下performConcurrentWorkOnRoot
举例:
function performConcurrentWorkOnRoot(root) {
//省略无关代码
const originalCallbackNode = root.callbackNode;
// 省略无关代码
ensureRootIsScheduled(root, now());
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
}
这段代码中我们可以看到,在callbackNode === originalCallBackNode
的时候会返回performConcurrentWorkOnRoot
本身,也即workLoop
中19~36行中的continuationCallback
。那么我们可以大概猜测callbackNode
值在ensureRootIsScheduled
函数中被修改了
从这里我们可以看到,callbackNode 是如何被赋值并且修改的。详细见15行,43行注释
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// Check if any lanes are being starved by other work. If so, mark them as
// expired so we know to work on those next.
markStarvedLanesAsExpired(root, currentTime);
// Determine the next lanes to work on, and their priority.
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// This returns the priority level computed during the `getNextLanes` call.
const newCallbackPriority = returnNextLanesPriority();
// 在fiber树构建、更新完成后。nextLanes会赋值为NoLanes 此时会将callbackNode赋值为null, 表示此任务执行结束
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
root.callbackNode = null;
root.callbackPriority = NoLanePriority;
}
return;
}
// 节流防抖
// Check if there's an existing task. We may be able to reuse it.
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
// The priority hasn't changed. We can reuse the existing task. Exit.
return;
}
// The priority changed. Cancel the existing callback. We'll schedule a new
// one below.
cancelCallback(existingCallbackNode);
}
// Schedule a new callback.
let newCallbackNode;
if (newCallbackPriority === SyncLanePriority) {
// Special case: Sync React callbacks are scheduled on a special
// internal queue
// 开始调度返回newCallbackNode,也即scheduler中的task.
newCallbackNode = scheduleSyncCallback(
performSyncWorkOnRoot.bind(null, root),
);
} else if (newCallbackPriority === SyncBatchedLanePriority) {
newCallbackNode = scheduleCallback(
ImmediateSchedulerPriority,
performSyncWorkOnRoot.bind(null, root),
);
} else {
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
newCallbackPriority,
);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
// 更新标记
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
到这里我们管中窥豹看到了中断渲染原理是如何做的,以及注册调度任务部分、节流防抖部分的代码。下面我们总结下:
消费任务队列的过程中, 可以消费1~n
个 task, 甚至清空整个 queue
. 但是在每一次具体执行task.callback
之前都要进行超时检测, 如果超时可以立即退出循环并等待下一次调用。
在时间切片的基础之上, 如果单个callback
执行的时间过长。就需要task.callback
在执行的时候自己判断下是否超时,所以concurrent
模式下,fiber树每构建完一个单元都会判断是否超时。如果超时则退出循环并返回回调,等待下次调用,完成之前没有完成的fiber
树构建。
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
其实上面的workLoop
中还有3个相对重要的函数没分析,这里我们简单看下
function advanceTimers(currentTime) {
// Check for tasks that are no longer delayed and add them to the queue.
// 检查过期任务队列中不应再被推迟的,放到taskQueue中
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
// Timer was cancelled.
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
// Timer fired. Transfer to the task queue.
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
if (enableProfiling) {
markTaskStart(timer, currentTime);
timer.isQueued = true;
}
} else {
// Remaining timers are pending.
return;
}
timer = peek(timerQueue);
}
}
function handleTimeout(currentTime) {
// 这个函数的作用是检查timerQueue中的任务,如果有快过期的任务,将它
// 放到taskQueue中,执行掉
// 如果没有快过期的,并且taskQueue中没有任务,那就取出timerQueue中的
// 第一个任务,等它的任务快过期了,执行掉它
isHostTimeoutScheduled = false;
// 检查过期任务队列中不应再被推迟的,放到taskQueue中
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
shouldYieldToHost
shouldYieldToHost = function() {
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
// There's no time left. We may want to yield control of the main
// thread, so the browser can perform high priority tasks. The main ones
// are painting and user input. If there's a pending paint or a pending
// input, then we should yield. But if there's neither, then we can
// yield less often while remaining responsive. We'll eventually yield
// regardless, since there could be a pending paint that wasn't
// accompanied by a call to `requestPaint`, or other main thread tasks
// like network events.
if (needsPaint || scheduling.isInputPending()) {
// There is either a pending paint or a pending input.
return true;
}
// There's no pending input. Only yield if we've reached the max
// yield interval.
return currentTime >= maxYieldInterval;
} else {
// There's still time left in the frame.
return false;
}
};
到这里我们大致阐述了react``Scheduler
任务调度循环的流程,以及时间切片和可中断渲染的原理。这部分是react
的核心,此外甚至在注册调度任务之前还做了节流和防抖等操作。由此我们看的核心的代码并不总是庞大的。respesct!!!
[1]PR: https://github.com/facebook/react/pull/16214
[2]见MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/MessageChannel
[3]源码: https://github.com/facebook/react/blob/v17.0.2/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L224-L230
[4]源码: https://github.com/facebook/react/blob/v17.0.2/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L232-L234
[5]源码: https://github.com/facebook/react/blob/v17.0.2/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L236-L240
[6]源码: https://github.com/facebook/react/blob/v17.0.2/packages/scheduler/src/forks/SchedulerHostConfig.default.js#L242-L245
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8