# 阅读图解React笔记5
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.port2channel.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 循环的退出条件:
-
队列被完全清空:正常的退出。
-
执行超时:在消费 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 并等待回调。
- 在 task 注册完成之后,会设置 FiberRoot 对象上的属性,代表现在已经处于调度进行中。
- 再次进入 ensureRootIsScheduled 时,如果发现处于调度中,则需要一些节流和防抖措施,进而保证调度性能。
- 节流(判断条件:existingCallbackPriority === newCallbackPriority,新旧更新的优先级相同),则无需注册新 task,直接退出调用。
- 防抖(判断条件:existingCallbackPriority !== newCallbackPriority,新旧更新的优先级不同),则取消旧 task,重新注册新 task。