# 阅读图解React笔记5

11 min read
Table of Contents

本文仅仅是阅读 图解 React 原理系列 的笔记,了解更多内容请查看原文链接。

调度原理

调度实现

调度中心最核心的代码,在 scheduler/src/forks/SchedulerHostConfig.default.js 中。该文件共导出 8 个函数。

/** 请求及时回调 */
export let requestHostCallback
/** 取消及时回调 */
export let cancelHostCallback
/** 请求延时回调 */
export let requestHostTimeout
/** 取消延时回调 */
export let cancelHostTimeout
/** 是否让出主线程 */
export let shouldYieldToHost
/** 请求绘制 */
export let requestPaint
/** 获取当前时间 */
export let getCurrentTime
/** 强制设置 yieldInterval */
export let forceFrameRate

调度相关

与调度相关的,请求或取消调度,它们的目的是请求执行(或取消)回调函数

  • requestHostCallback
  • cancelHostCallback
  • requestHostTimeout
  • cancelHostTimeout
const performWorkUnitlDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime()
// 在 yieldInterval 毫秒后产生,无论我们在 vsync 周期中的哪个位置
// 这意味着在消息时间开始时总是有剩余时间
deadline = currentTime + yieldInterval
const hasTimeRemaining = true
try {
// 执行回调,返回是否还有剩余任务
const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime)
if (!hasMoreWork) {
isMessageLoopRunning = false
scheduledHostCallback = null
} else {
// 如果还有更多的工作,在前一个消息事件的末尾安排下一个消息事件
port.postMessage(null)
}
} catch (error) {
// 如果调度任务抛错,退出当前浏览器任务以便观察错误
port.postMessage(null)
throw error
}
} else {
isMessageLoopRunning = false
}
needsPaint = false
}
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = performWorkUnitlDeadline
requestHostCallback = function (callback) {
scheduledHostCallback = callback
if (!isMessageLoopRunning) {
isMessageLoopRunning = true
port.postMessage(null)
}
}
cancelHostCallback = function () {
scheduledHostCallback = null
}
requestHostTimeout = function (callback, ms) {
taskTimeoutId = setTimeout(() => {
callback(getCurrentTime())
}, ms)
}
cancelHostTimeout = function () {
clearTimeout(taskTimeoutId)
taskTimeoutId = -1
}

请求回调之后 scheduledHostCallback = callback,然后通过 MessageChannel 发消息的方式触发 performWorkUnitlDeadline,最后执行回调 scheduledHostCallback。

需要注意:MessageChannel 在浏览器事件循环中属于宏任务,所以调度中心用于是异步执行回调函数。

时间切片

时间切片(time slicing),执行时间分割,让出主线程(把控制权归返浏览器,浏览器可以处理用户输入,UI 绘制等紧急任务)

  • getCurrentTime
  • shouldYieldToHost
  • requestPaint
  • forceFrameRate
if (
enableIsInputPending &&
navigator !== undefined &&
navigator.scheduler !== undefined &&
navigator.scheduler.isInputPending !== undefined
) {
const scheduling = navigator.scheduling
shouldYieldToHost = function () {
const currentTime = getCurrentTime()
if (currentTime >= deadline) {
if (needsPaint || scheduling.isInputPending()) {
return true
}
return currentTime >= maxYieldInterval
} else {
return false
}
}
requestPaint = function () {
needsPaint = true
}
} else {
shouldYieldToHost = function () {
return getCurrentTime >= deadline
}
requestPaint = function () {}
}
forceFrameRate = function (fps) {
if (fps < 0 || fps > 125) {
console['error'](
'forceFrameRate takes a positive int between 0 and 125, ' +
'forcing frame rates higher than 125 fps is not supported',
)
return
}
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps)
} else {
yieldInterval = 5
}
}

shouldYieldToHost 的判定条件:

  • currentTime >= deadline(currentTime + yieldInterval),只有时间超过 deadline 之后才会让出主线程。
    • yieldInterval 默认是 5ms,只能通过 forceFrameRate 函数进行修改。
    • 如果一个 task 运行时间超过 5ms,下一个 task 执行之前,会把控制权归还浏览器。
  • navigator.scheduling.isInputPending,用于判断是否有输入事件。

从上面可以得到调度中心的内核实现图:

任务队列管理

调度的目的是为了消费任务,接下来看看任务队列的管理和实现。

在 scheduler/src/Scheduler.js 维护一个 taskQueue,任务队列管理主要就是围绕 taskQueue 展开。

// 任务队列保存在一个最小堆里
var taskQueue = []
var timerQueue = []

创建任务

在 scheduler/src/Scheduler.js 中的 unstable_schedulerCallback 函数:

function unstable_schedulerCallback(priorityLevel, callback, options) {
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
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 LowPirority:
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 (startTime > currentTime) {
// 这是个延迟任务
newTask.sortIndex = startTime
push(timerQueue, newTask)
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// 所有的任务都是延迟的,且该任务是最先被延迟的
if (isHostTimeoutScheduled) {
cancelHostTimeout()
} else {
isHostTimeoutScheduled = true
}
requestHostTimeout(handleTimeout, startTime - currentTime)
}
} else {
newTask.sortIndex = expirationTime
push(taskQueue, newTask)
// 省略无关代码
// 请求调度
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true
requestHostCallback(flushWork)
}
}
return newTask
}

消费任务

创建任务之后,最后请求调度 requestHostCallback(flushWork),flushWork 函数作为参数传入调度中心内核等待回调。

function flushWork(hasTimeRemaining, initialTime) {
// 省略无关代码
// 做好全局标记,表示现在已经进入调度阶段
isHostCallbackScheduled = false
if (isHostTimeoutScheduled) {
isHostTimeoutScheduled = false
cancelHostTimeout()
}
isPerformingWork = true
const previousPriorityLevel = currentPriorityLevel
try {
// 循环消费队列
return workLoop(hasTimeRemaining, initialTime)
} finally {
// 还原全局标记
currentTask = null
currentPriorityLevel = previousPriorityLevel
isPerformingWork = false
}
}

flushWork 中调用了 workLoop,队列消费的主要逻辑是在 workLoop 函数中,这就是 React 的任务调度循环。

function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime
advanceTimers(currentTime)
currentTask = peek(taskQueue)
while (currentTask !== null) {
if (
currentTask.expirationTime > currentTask &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// 当前任务未过期,但是达到时间限制
break
}
const callback = currentTask.callback
if (typeof callback === 'function') {
currentTask.callback = null
currentPriorityLevel = currentTask.priorityLevel
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime
const continuationCallback = callback(didUserCallbackTimeout)
currentTime = getCurrentTime()
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue)
}
}
advanceTimers(currentTime)
} else {
pop(taskQueue)
}
currentTask = peek(taskQueue)
}
// 返回,不管是否有额外的工作
if (currentTask !== null) {
return true
} else {
const firstTimer = peek(timerQueue)
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime)
}
return false
}
}

workLoop 就是一个大循环,在此处实现了时间切片和 fiber 树的可中断渲染。这 2 个大特性的实现,都集中于这个 while 循环。

每一次 while 循环的推出就是一个时间切片,深入分析 while 循环的退出条件:

  1. 队列被完全清空:正常的退出。

  2. 执行超时:在消费 taskQueue 时,在执行 task.callback 之前,都会检测是否超时,所以超时检测是以 task 为单位。

    如果某个 task.callback 执行时间太久也会超时(如 fiber 树过大,逻辑很重),这时使用检测机制,超时了立刻暂停 task.callback 的执行

时间切片原理

消费任务队列的过程中,可以消费 1~n 个 task,甚至清空整个 queue。但是在每一次具体执行 task.callback 之前都要进行检测,如果超时可以立即推出循环并等待下一次调用。

可中断渲染

在时间切片的基础之上,如果单个 task.callback 执行时间就很长(假设 200ms)。就需要 task.callback 自己能够检测是否超时,所以在 fiber 树构建过程中,每构建完成一个单元,都会检测一次超时,如遇超时就会退出 fiber 树循环构建,并返回一个新的回调函数(就是 continuationCallback)并等待下一次回调继续未完成的 fiber 树构造。

节流防抖

在 reconciler 运作流程中的总结的 4 个阶段中,注册调度任务属于第 2 个阶段,核心逻辑位于 react-reconciler/src/ReactFiberWorkLoop.new.js ensureRootIsScheduled 函数中。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// 检测是否有车道被其他工作占用,如果有,将它们标记为已过期
markStarvedLanesAsExpired(root, currentTime);
// 确定下一个要处理的的车道及其优先级
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
if (nextLanes === NoLanes) {
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
root.callbackNode = null;
root.callbackPriority = NoLanePriority;
}
return;
}
// 节流防抖
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
return;
}
cancelCallback(existingCallbackNode);
}
// 调度一个新的任务
let newCallbackNode;
if (newCallbackPriority === SyncLanePriority) {
newCallbackNode = scheduledHostCallback(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;
}

正常的情况下,ensureRootIsScheduled 函数会与 scheduler 包通信,最后注册一个 task 并等待回调。

  1. 在 task 注册完成之后,会设置 FiberRoot 对象上的属性,代表现在已经处于调度进行中。
  2. 再次进入 ensureRootIsScheduled 时,如果发现处于调度中,则需要一些节流和防抖措施,进而保证调度性能。
    1. 节流(判断条件:existingCallbackPriority === newCallbackPriority,新旧更新的优先级相同),则无需注册新 task,直接退出调用。
    2. 防抖(判断条件:existingCallbackPriority !== newCallbackPriority,新旧更新的优先级不同),则取消旧 task,重新注册新 task。

参考链接


More Posts