- 第一篇文章React源码入门篇(一)我们主要从
React.createRoot()
和ReactDom.reader
中的render
和updateContainer
函数入手, 在updateContainer
函数中最后调用了scheduleUpdateOnFiber
, 走入了我们的第二部分
这篇文章就从scheduleUpdateOnFiber
开始探索Scheduler
代码大部分来源于react/blob/v18.2.0/packages/scheduler/src/forks/Scheduler.js
文章的脉络直接依照文章一总结的执行栈顺序
- scheduleUpdateOnFiber()
- ensureRootIsScheduled()
- unstable_scheduleCallback()
- requestHostCallback()
- schedulePerformWorkUntilDeadline()
- performWorkUntilDeadline()
- flushWork()
- workLoop()
- performConcurrentWorkOnRoot()
一、scheduleUpdateOnFiber
这个函数还是相当多内容的, 但是我们直接抓重点看就好了, 这里有两个比较重要的函数, 一个是markRootUpdated
, 一个是ensureRootIsScheduled
js
function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
checkForNestedUpdates();
// 标记FiberRootNode的pendingLanes
markRootUpdated(root, lane, eventTime);
.....
//
ensureRootIsScheduled(root, eventTime);
....
}
markRootUpdated
函数, 其中最主要的逻辑就是将前面获取到的优先级lane
挂载到FiberRootNode
的pendingLanes
属性上, 表示当前有需要更新的任务。然后再计算出其优先级对应的赛道下标。更新起始时间。
js
function markRootUpdated(root, updateLane, eventTime) {
// 进行按位与并赋值
root.pendingLanes |= updateLane;
// 如果当前优先级非空闲优先级的话则重置Suspence相关优先级
if (updateLane !== IdleLane) {
root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
}
// 拿到事件起始时间
var eventTimes = root.eventTimes;
// 采用31 - clz32(lanes)的方式, 当前传入lanes16时,拿到赛道下标index为4
var index = laneToIndex(updateLane);
// 记录evnetTimes
eventTimes[index] = eventTime;
}
二、ensureRootIsScheduled
这个函数很重要, 多次被调用。 主要是分成三个部分
- 第一部分通过
markStarvedLanesAsExpired
处理饥饿问题 - 第二部分通过
getNextLanes
确定下一个更新任务的优先级 - 第三部分通过
scheduleCallback
,cancelCallback
这两个函数根据拿到的优先级调度任务(这部分需要引入后面的内容才能更具体的描述, 后面会总结)
js
function ensureRootIsScheduled(root, currentTime) {
// 取出FiberRootNode的callbackNode
var existingCallbackNode = root.callbackNode;
// 遍历待处理的更新任务,检查是否已经达到了它们的过期时间或者计算他们的过期时间
// 如果是,就将这些任务标记为过期,在后面的逻辑再对针对他们进行处理
markStarvedLanesAsExpired(root, currentTime);
// 确定下一个更新任务的优先级
var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
// 如果取出来为空, 则表示没有任务需要运行了
if (nextLanes === NoLanes) {
// 如果当前有任务再进行的话, 则取消该task
if (existingCallbackNode !== null) {
cancelCallback$1(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// 下一个任务优先级
var newCallbackPriority = getHighestPriorityLane(nextLanes);
// 当前优先级
var existingCallbackPriority = root.callbackPriority;
....
// 如果前后优先级一致的话, 则直接重用当前任务, 不发起新的调度任务, 故直接return
if (existingCallbackPriority === newCallbackPriority &&
!( ReactCurrentActQueue$1.current !== null && existingCallbackNode !== fakeActCallbackNode)) {
return;
}
// 当不一致的时候, 取消之前的callback任务
if (existingCallbackNode != null) {
cancelCallback$1(existingCallbackNode);
}
// 创建新的callback task
var newCallbackNode;
// 如果新的回调优先级是同步优先级(SyncLane),则特殊处理
if (newCallbackPriority === SyncLane) {
// 如果根节点的标记是LegacyRoot,则将执行同步工作的函数调度到同步回调队列中
if (root.tag === LegacyRoot) {
....
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
....
} else {
var schedulerPriorityLevel;
// 对优先级进行Switch Case的转换, 将lane优先级转换为事件优先级
// 然后再转换为Scheduler的优先级
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediatePriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdlePriority;
break;
default:
schedulerPriorityLevel = NormalPriority;
break;
}
// 开启新的调度任务
newCallbackNode = scheduleCallback$1(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
// 更新root节点上的信息
root.callbackPriority = newCallbackPriority;
// 这里其实就是scheduleCallback内部创建的newTask
root.callbackNode = newCallbackNode;
}
markStarvedLanesAsExpired
ensureRootIsScheduled
其中的markStarvedLanesAsExpired
也算比较重要的一环。 主要逻辑分成两步
- 在没有设置
expirationTime
的情况下进行根据currentTime
和优先级lanes
计算出过期时间 - 在有设置的情况下判断是否过期的, 过期的话保存到
expiredLanes
。
所以他的任务就是检查并标记那些因为长时间未完成而被认为是饥饿状态的更新任务,以确保它们能够得到及时处理
js
function markStarvedLanesAsExpired(root, currentTime) {
// 先拿到各种更新任务优先级
var pendingLanes = root.pendingLanes;
var suspendedLanes = root.suspendedLanes;
var pingedLanes = root.pingedLanes;
var expirationTimes = root.expirationTimes;
var lanes = pendingLanes;
while (lanes > 0) {
// 算出expirationTime对应的赛道下标index
var index = pickArbitraryLaneIndex(lanes);
var lane = 1 << index;
// 拿到FiberRootNode上的expirationTimes对应下标的过期时间
// 默认为-1, 则NoTimestamp
var expirationTime = expirationTimes[index];
// 没有设置expirationTime的情况
if (expirationTime === NoTimestamp) {
// 如果当前不是挂起优先级和重启优先级,则计算出其过期时间
if ((lane & suspendedLanes) === NoLanes || (lane & pingedLanes) !== NoLanes) {
// 根据当前时间和优先级计算出其过期时间
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
// 如果已经过期
} else if (expirationTime <= currentTime) {
// 根据按位或赋值在root.expiredLanes,
root.expiredLanes |= lane;
}
// 文章一介绍过,拿到下一个优先级, 继续处理
lanes &= ~lane;
}
}
- 其中计算过期的时间代码如下, 可以看到, 其实就是主要分成三种
- 对于连续的优先级,在
currentTime
的基础上再加250毫秒 - 默认优先级或过渡相关优先级则加5000毫秒
- 其他不设置过期时间
- 对于连续的优先级,在
js
function computeExpirationTime(lane: Lane, currentTime: number) {
switch (lane) {
case SyncHydrationLane:
case SyncLane:
case InputContinuousHydrationLane:
case InputContinuousLane:
return currentTime + 250;
case DefaultHydrationLane:
case DefaultLane:
case TransitionHydrationLane:
case TransitionLane1:
case TransitionLane2:
case TransitionLane3:
case TransitionLane4:
case TransitionLane5:
case TransitionLane6:
case TransitionLane7:
case TransitionLane8:
case TransitionLane9:
case TransitionLane10:
case TransitionLane11:
case TransitionLane12:
case TransitionLane13:
case TransitionLane14:
case TransitionLane15:
return currentTime + 5000;
case RetryLane1:
case RetryLane2:
case RetryLane3:
case RetryLane4:
return NoTimestamp;
case SelectiveHydrationLane:
case IdleHydrationLane:
case IdleLane:
case OffscreenLane:
case DeferredLane:
return NoTimestamp;
default:
return NoTimestamp;
}
}
getNextLanes
-
获取下一任务:
getNextLanes
通过lanes
找到下一个调度的更新任务, 主要看是否处于空闲任务, 然后找到最高优先级, 接着跟当前优先级进行比较,如果当前正在处理任务且新的任务优先级不高于当前任务优先级,则返回当前任务优先级, 否则的话返回新任务优先级jsfunction getNextLanes(root, wipLanes) { // 取出待更新任务优先级 var pendingLanes = root.pendingLanes; // 如果等于NoLanes的话则说明当前没有任务在等待更新了 if (pendingLanes === NoLanes) { return NoLanes; } // 初始化 var nextLanes = NoLanes; var suspendedLanes = root.suspendedLanes; var pingedLanes = root.pingedLanes; // 获取非空闲任务的待处理任务优先级 var nonIdlePendingLanes = pendingLanes & NonIdleLanes; // 如果存在非空闲任务 if (nonIdlePendingLanes !== NoLanes) { // 获取非空闲未被阻塞的任务优先级 var nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes; if (nonIdleUnblockedLanes !== NoLanes) { // 获取非空闲未被阻塞的任务优先级中最高的优先级 nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes); } else { // 这里一样的道理 var nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes; if (nonIdlePingedLanes !== NoLanes) { nextLanes = getHighestPriorityLanes(nonIdlePingedLanes); } } } else { // 只剩下空闲任务等待执行, 也是去找到最高位的 var unblockedLanes = pendingLanes & ~suspendedLanes; if (unblockedLanes !== NoLanes) { nextLanes = getHighestPriorityLanes(unblockedLanes); } else { if (pingedLanes !== NoLanes) { nextLanes = getHighestPriorityLanes(pingedLanes); } } } // 如果上面两个分支都没有走到的话就直接reuturn出去了 if (nextLanes === NoLanes) { return NoLanes; } // 如果当前正在处理任务且新的任务优先级不高于当前任务优先级,则继续处理当前任务。 if ( wipLanes !== NoLanes && wipLanes !== nextLanes && (wipLanes & suspendedLanes) === NoLanes ) { var nextLane = getHighestPriorityLane(nextLanes); var wipLane = getHighestPriorityLane(wipLanes); if ( nextLane >= wipLane || nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes ) { // 不打断, 保持吃当前优先级 return wipLanes; } } // 如果下一个任务优先级包含连续输入任务,则将默认任务添加到下一个任务优先级中。 if ((nextLanes & InputContinuousLane) !== NoLanes) { nextLanes |= pendingLanes & DefaultLane; } .... return nextLanes; }
cancelCallback
主要逻辑就是将task
的callback
置为空
js
function unstable_cancelCallback(task) {
if (enableProfiling) {
if (task.isQueued) {
const currentTime = getCurrentTime();
markTaskCanceled(task, currentTime);
task.isQueued = false;
}
}
task.callback = null;
}
scheduleCallback
调度任务: 这里分成同步和并发, 通过newCallbackPriority === SyncLanes
进行区分
- 同步的话调用的是
scheduleSyncCallback
, 会相对简单一些, 因为不考虑中断重启之类的。 故直接就是维护一个syncQueue
队列, 有则入队列。然后再按顺序拿出来执行即可
js
export function scheduleSyncCallback(callback: SchedulerCallback) {
if (syncQueue === null) {
syncQueue = [callback];
} else {
syncQueue.push(callback);
}
}
- 并发的话调用的是
unstable_scheduleCallback
三、unstable_scheduleCallback
并发的话调用的是unstable_scheduleCallback
, 相对于同步逻辑会比较多, 因为要考虑调度情况。 这里其实就是生成了一个task
对象,然后根据startTime
和currentTime
的对比, 判断要推入timeQueue
还是taskQueue
, 推入timeQueue
的话调用requestHostTimeout
,否则调用requestHostCallback
js
function unstable_scheduleCallback(priorityLevel, callback, options) {
// ...前面是处理newTask的各个属性值, 这里就直接忽略了
var newTask = {
id: taskIdCounter++,
callback: callback, // 我们传入的performConcurrentWorkOnRoot函数
priorityLevel: priorityLevel, // 优先级
startTime: startTime, // 开始时间
expirationTime: expirationTime, // 过期时间
sortIndex: -1
};
// 还没到时间,startTime还没到, 推入timeQueue
if (startTime > currentTime) {
newTask.sortIndex = startTime;
push(timerQueue, newTask); // 将新任务推入timeQueue延时队列
// taskQueue[0]为null且timeQueue[0]为当前task
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
// 有新的任务推进来的话则取消之前的定时,反正会开新的
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 开启新的定时进行处理
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 到点了, 推入taskQueue
newTask.sortIndex = expirationTime;
push(taskQueue, newTask); // 推入taskQueue
// 如果如果没有任务在调度,并且当前不在执行工作状态
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork); // 开启host回调
}
}
return newTask; // 将创建好的task返回出去
}
push
-
其中
push()
函数也值得了解一下,他不是单纯的推入操作, 他push
进去之后又调用了siftUp()
实现堆上浮的操作,类似于找到最小堆, 其中对比调用的是compare()
, 先对比了sortIndex
, 我们从上面可以看到他分情况赋值,推入timeQueue
赋值的是startTime
, 推入taskQueue
推入的是expirationTime
, 如果一致的话则退级通过id
进行对比, 那当我们通过peek(taskQueue)
或者peek(timeQueue)
拿取的时候总能拿到最高优先级的jsexport function push(heap: Heap, node: Node): void { const index = heap.length; heap.push(node); siftUp(heap, node, index); } function siftUp(heap, node, i) { let index = i; while (index > 0) { const parentIndex = (index - 1) >>> 1; const parent = heap[parentIndex]; if (compare(parent, node) > 0) { // The parent is larger. Swap positions. heap[parentIndex] = node; heap[index] = parent; index = parentIndex; } else { return; } } } function compare(a, b) { // Compare sort index first, then task id. const diff = a.sortIndex - b.sortIndex; return diff !== 0 ? diff : a.id - b.id; }
requestHostTimeout
可以看到他就是开启了一个定时, 延时的时间为startTime - currentTime
,回调的函数为handleTimeout
。 再看handleTimeout
, 其实就是因为我们的定时是s-c
, 故时间到了一般意味着该任务可以从timeQueue
移到taskQueue
了, 故一开始我们判断了是否可以移入。 然后判断taskQueue
是否有任务, 有的话则开启调度。 没有的话再继续判断timeQueue
是否有任务。 有的话则继续开启定时
js
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime); // 先判断timeQueue中是否有任务可以移入taskQueue
if (!isHostCallbackScheduled) {
// taskQueue存在任务
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork); // 开启调度
} else {
const firstTimer = peek(timerQueue); // 拿到timeQueue中的第一个任务
if (firstTimer !== null) {
// 继续开启定时
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
advanceTimer
- 其中的
advanceTimer
函数, 作用其实就是找一找timerQueue
有没有能够转移进来taskQueue
的, 通过判断Queue
中的startTime
和currentTime
进行对比
js
function advanceTimers(currentTime) {
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {// 无效的say拜拜
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
// 到点了, 从timerQueue移出, 转入taskQueue
pop(timerQueue);
// 这里要重置sortIndex, 原因我们上面也讲过了
// 对应taskQueue来说sortIndex存放的就是expirationTime, 而timeQueue是startTime
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
..
} else {
return; // 排在最前面的都还没有到点, 后面的自然也没到, 直接return走人
}
timer = peek(timerQueue); // 挨个找出来判断
}
}
requestHostCallback
-
接着我们可以看到
unstable_scheduleCallback
中最后是调用了requestHostCallback
去开启host
回调, 其中又去调用了schedulePerformWorkUntilDeadline()
;jsfunction requestHostCallback(callback) { scheduledHostCallback = callback; if (!isMessageLoopRunning) { isMessageLoopRunning = true; schedulePerformWorkUntilDeadline(); } }
四、schedulePerformWorkUntilDeadline
我们通过打断点可以看到schedulePerformWorkUntilDeadline
最终内部就是做了一个postMessage
的操作, 且上下文new
了一个MessageChannel
MessageChannel
我们可以先学一波什么是MessageChannel
- Channel Messaging API 的
MessageChannel
接口允许我们创建一个新的消息通道,并通过它的两个MessagePort
属性发送数据 - Channel Messgae属于宏任务
- 来段代码小试一下
js
const { port1 , port2 } = new MessageChannel();
port1.postMessage('123')
port2.onmessage = (msg) => {
console.log('收到:' + msg.data);
}
Promise.resolve().then(() => { console.log('微任务啦')});
console.log('同步任务啦');
所以截图中的代码就是先实例化了一个MessageChannel
, 然后port1
开启onmessage
监听, 回调函数是performWorkUntilDeadline
, 然后我们在schedulePerformWorkUntilDeadline
中去使用port2
通过postMessage
的方式发送消息, 目的是为了触发performWorkUntilDeadline
在后面的宏任务执行
我们录制一下performance
也可以看到在执行完schedulePerformWorkUntilDeadline
之后就交出主线程,浏览器做渲染的操作, 然后再执行宏任务触发了performWorkUntilDeadline
五、performWorkUntilDeadline
- 可以看到他的核心逻辑就是调用了
scheduledHostCallback
, 无论成功还是失败再去再判断haseMoreWork
去判断要调用schedulePerformWorkUntilDeadline
还是结束
js
const performWorkUntilDeadline = () => {
// 所以只要前面调用了requestHostCallback的话这里就不会为null
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
startTime = currentTime;
const hasTimeRemaining = true;
let hasMoreWork = true;
try {
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
// 这里做判断
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
}
needsPaint = false;
};
- 你会发现, 只要
hasMoreWork
为true
的时候, 他们之间就形成了循环
- 那这里的
scheduledHostCallback
是什么呢, 我们可以回看requestHostCallback
函数, 内部有逻辑就是将传入的flushWork
赋值给了scheduledHostCallback
六、flushWork
flushWork
的逻辑, 可以看到就是一些状态的重置, 然后核心逻辑又走向了workLoop
js
function flushWork(hasTimeRemaining, initialTime) {
// 将isHostCallbackScheduled赋值为false
// 我们曾在scheduleCallback的时候判断了它后将其赋值为true才开启了requestHostCallback
// 这里重置状态
isHostCallbackScheduled = false;
// ...
isPerformingWork = true; // 修改状态为执行中
const previousPriorityLevel = currentPriorityLevel; // 设置优先级
try {
....
return workLoop(hasTimeRemaining, initialTime);
} finally {
currentTask = null; // 重置状态
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false; //执行完毕
...
}
}
七、workLoop
- 看到函数的命名中带有
Loop
就知道十有八九又来个循环, 果不其然, 内部又使用了while, 那是什么在循环呢, 还记得我们在scheduleCallback
生成了newtask
, 并且将task
存入taskQueue
和timeQueue
, 那么现在就是考虑将他拿出来执行的时候啦
js
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
// 在找taskQueue之前先再找一遍timeQueue看看有没有老铁到点了能转移进来的
advanceTimers(currentTime);
// 拿到当前要执行的任务
currentTask = peek(taskQueue);
while (
currentTask !== null && //当前有任务
!(enableSchedulerDebugging && isSchedulerPaused)
) {
// 1. task任务还没过期
// 2. hasTimeRemaining为false或者shouldYieldToHost()为true
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// 这种情况下就是属于task还没有过期, 但是没有剩余的可执行时间了,故跳出
break;
}
// 拿出回调, 这个回调其实就是之前我们存放进去的performConcurrentWorkOnRoot
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();
// 如果还有任务的话则类型为function, 那么此时会重新给currentTask.callback赋值
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
...
} else {
...
// 没有接下去的任务的话该可以将该任务从taskQueue取出了
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
// 处理完一项任务后重新找一遍timeQueue看看有没有能移入taskQueue的
advanceTimers(currentTime);
} else {
// callback为空的情况则是因为通过cancelCallback取消了该任务, 故不用处理, 直接移出taskQueue即可
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// 跳出循环有两种可能
// 一种是没有currenTask了,taskQueue中的任务都执行完了
// 一种就是还有task但是执行时间没有了break出来的
if (currentTask !== null) {
return true; //这种就是还有任务, return true表示后续还有任务待执行
} else {
....
return false; //return false表示后面没有任务了
}
}
workLoop
拿到的task
的callback
指的就是performConcurrentWorkOnRoot
八、 performConcurrentWorkOnRoot
从函数的命名也可以看出, 这个函数就是真正的去执行task
的过程, 他先再调用了一次getNextLanes
,确保此时拿到的lanes
确实是最高优先级的, 然后再通过shouldTimeSlice
判断进行饥饿处理和进入render
节点,拿到exitStatus
后,判断exitStatus
进去commit
阶段
js
function performConcurrentWorkOnRoot(root, didTimeout) {
...
const originalCallbackNode = root.callbackNode;
// getNextLanes我们上面接触过了, 就是获取下一个任务的优先级
// 那么在这里再调用一次的原因我理解是在执行任务前可能又有更高优先级的任务
// 那么可以执行的任务是更高优先级的任务
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// 没有等待的任务了
if (lanes === NoLanes) {
return null;
}
// shouldTimeSlice根据lane的优先级,决定是采用并发模式还是同步模式渲染,需要同时满足
// 1. 不包含被阻塞的优先级
// 2. 不包含过期的优先级
// 3. 是否过期
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
// 这个就是通过root.expiredLanes去判断,我们在markStarvedLanesAsExpired处理过
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
// 接下去的代码就是通过调用renderRootxx()返回的exitStatus的不同情况做不同的处理了
// 这里先省略后面有解释
....
ensureRootIsScheduled(root, now());
// 检查根节点的回调是否发生了变化---在文章三会详细讲这里
if (root.callbackNode === originalCallbackNode) {
// 如果没有变化,则返回一个继续执行的回调函数
return performConcurrentWorkOnRoot.bind(null, root);
}
// 否则返回null
return null;
}
-
其中的
exitStatus
有几种情况RootInProgress = 0
: 表示渲染正在进行中, 无需进一步处理RootFatalErrored = 1
: 表示渲染过程出现致命错误, 抛出错误并终止程序
jsif (exitStatus === RootFatalErrored) { const fatalError = workInProgressRootFatalError; prepareFreshStack(root, NoLanes); markRootSuspended(root, lanes); ensureRootIsScheduled(root, now()); throw fatalError; // 直接抛出错误了 }
RootErrored = 2
:表示渲染过程中出现错误, 尝试再次渲染, 第二次还是失败的话则放弃
jsif (exitStatus === RootErrored) { const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root); if (errorRetryLanes !== NoLanes) { lanes = errorRetryLanes; exitStatus = recoverFromConcurrentError(root, errorRetryLanes); } }
RootDidNotComplete = 6
: 表示在未完成树的情况下展开, 进行标记, 这种情况只会发生在并发渲染模式, 让出主线程让浏览器执行更高优先级的任务。
jsif (exitStatus === RootDidNotComplete) { markRootSuspended(root, lanes); }
RootSuspended = 3
: 挂起了RootSuspendedWithDelay = 4
: 这个没了解到RootCompleted = 5
: 正常渲染完成
js{ // The render completed. ... // render的流程结束后就要走向commit的流程 // We now have a consistent tree. The next step is either to commit it, // or, if something suspended, wait to commit it after a timeout. root.finishedWork = finishedWork; root.finishedLanes = lanes; finishConcurrentRender(root, exitStatus, lanes); }
-
renderRootConcurrent
和renderRootSync
其实都是render
的过程了,我们放在下一章继续讲解render相关流程 -
finishConcurrentRender
作为commit
的入口我们放在第四章继续讲解commit
相关流程
附上面函数流程图的总结
简单图示
上面的有点乱, 再来一张纯净版的
- 这个就很明显了
- 红色的表示执行栈中的主函数
- 绿色表示函数存在的一些主逻辑
- 蓝色表示返回值(这里主要为了
hasMoreWork
) - 可以看到基本就是大循环里面嵌套小循环
九、调度总结
学习完上面的代码已经对调度有了一定的了解。 接下去从三个维度去复习总结调度过程(可以回看之前的代码逻辑, 都有涉及,此部分是个将上面的代码逻辑串联起来)
高优先级触发取消相关
- 取消的原因: 有更高优先级的事件插入
- 取消的逻辑:主要逻辑我们在
ensureRootIsScheduled
中学习过, 使用的函数是cancelCallback
, 核心逻辑是将task
的callback
置为空。 - 取消效果如何实现: 在
workLoop
中将taskQueue
中task
拿出来处理时, 如果task
的callback
为空的话,直接移出,不处理
打断相关
- 打断原因: 释放线程给浏览器
- 打断逻辑: 在
workLoop
中我们通过判断了三个条件。 分别为currentTask.expirationTime > currentTime
判断任务是否过期。hasTimeRemaining
怕电脑是否有剩余时间。shouldYieldToHost
判断是否还有时间片。 判断如果需要让出线程的话则break
,此时不再处理taskQueue
的任务, 但是因为当前是存在任务的, 故schedulePerformWorkUntilDeadline
又会创建新的宏任务去处理任务。 故就成功做到了打断,后面又能顺利重启
饥饿相关
- 饥饿处理的原因: 因为我们存在可以打断低优先级然后执行高优先级任务的逻辑。 故可能引发一种情况: 一直有高优先级的任务存在导致低优先级的任务迟迟无法被执行
- 饥饿处理的逻辑: 我们在前面的逻辑中两次提到饥饿处理
- 第一是在
ensureRootIsScheduled
中调用了markStarvedLanesAsExpired
, 根据pendinglanes
计算出了当前存在过期现象的赛道, 并记录到expiredLanes
- 第二是在
performConcurrentWorkOnRoot
通过expiredLanes
作为条件之一进行了判断,若存在expiredLanes
则调用renderRootSync
, 否则调用renderRootConcurrent
. 其中的差异我们下一章会涉及
- 第一是在