React源码04 - Fiber Scheduler (调度器
创建更新之后,找到 Root 然后进入调度,同步和异步操作完全不同,实现更新分片的性能优化。
主流的浏览器刷新频率为 60Hz,即每(1000ms / 60Hz)16.6ms 浏览器刷新一次。JS可以操作 DOM,JS线程
与 GUI渲染线程
是互斥的。所以 **JS脚本执行 **和 **浏览器布局、绘制 **不能同时执行。
在每16.6ms时间内,需要完成如下工作:
JS脚本执行 ----- 样式布局 ----- 样式绘制
既然以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器在每一帧 16.6ms 中执行完自己的 GUI 渲染线程后,还有剩余时间的话能通知我们执行 react 的异步更新任务,react 执行时会自己计时,如果时间到了,而 react 依然没有执行完,则会挂起自己,并把控制权还给浏览器,以便浏览器执行更高优先级的任务。然后 react 在下次浏览器空闲时恢复执行。而如果是同步任务,则不会中断,会一直占用浏览器直到页面渲染完毕。
其实部分浏览器已经实现了这个API,这就是 requestIdleCallback(字面意思:请求空闲回调)。但是由于以下因素,React 放弃使用:
- 浏览器兼容性
- 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的 requestIdleCallback 触发的频率会变得很低。
React 实现了功能更完备的 requestIdleCallback polyfill(使用window.requestAnimationFrame()
和 JavaScript 任务队列进行模拟),这就是 Scheduler,除了在空闲时触发回调的功能外,Scheduler 还提供了多种调度优先级供任务设置。
当 Scheduler 将任务交给 Reconciler 后,Reconciler 会为变化的虚拟 DOM 打上代表增/删/更新的标记,类似这样:
// 这种二进制存储数据:
// 设置:集合 | 目标
// 查询:集合 & 目标
// 取消:集合 & ~目标
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
整个 Scheduler 与 Reconciler 的工作都在内存中进行。只有当所有组件都完成 Reconciler 的工作后,才会统一交给 Renderer。
Renderer 根据 Reconciler 为虚拟 DOM 打的标记,同步执行对应的 DOM 操作。
image
isBatchingUpdates
变量在早前的调用栈中(我们为 onClick 绑定的事件处理函数会被 react 包裹多层),被标记为了 true
,然后 fn(a, b)
内部经过了3次 setState
系列操作,然后 finally 中 isBatchingUpdates
恢复为之前的 false,此时执行同步更新工作 performSyncWork
:
第2种:
在 handleClick
中使用 setTimeout
将 this.countNumber
包裹了一层 setTimeout(() => { this.countNumber()}, 0)
,同样要调用 handleClick
也是先经过 interactiveUpdates$1
上下文,也会执行 setTimeout
,然后 fn(a, b)
就执行完了,因为最终是浏览器来调用 setTimeout
的回调 然后执行里面的 this.countNumber
,而对于 interactiveUpdates$1
来说继续把自己的 performSyncWork
执行完,就算结束了。显然不管 performSyncWork
做了什么同步更新,我们的 setState
目前为止都还没得到执行。然后等到 setTimeout
的回调函数等到空闲被执行的时候,才会执行 setState
,此时没有了批量更新之上下文,所以每个 setState
都会单独执行一遍 requestWork
中的 performSyncWork
直到渲染结束,且不会被打断,3次 setState
就会整个更新渲染 3 遍(这样性能不好,所以一般不会这样写 react)。
什么叫不会被打断的同步更新渲染?看一下 demo 中的输出,每次都同步打印出了最新的 button dom 的 innerText
。
第3种:
已经可以猜到,无非就是因为使用 setTimeout
而“错过了”第一次的批量更新上下文,那等到 setTimeout
的回调执行的时候,专门再创建一个批量更新上下文即可:
image
**
继续之前的源码,requestWork 的最后,如果不是同步的更新任务,那么就要参与 Scheduler 时间分片调度了:
// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) {
performSyncWork();
} else {
scheduleCallbackWithExpirationTime(root, expirationTime);
}
scheduleCallbackWithExpirationTime:
function scheduleCallbackWithExpirationTime(
root: FiberRoot,
expirationTime: ExpirationTime,
) {
// 如果已经有在调度的任务,那么调度操作本身就是在循环遍历任务,等待即可。
if (callbackExpirationTime !== NoWork) {
// A callback is already scheduled. Check its expiration time (timeout).
// 因此,如果传入的任务比已经在调度的任务优先级低,则返回
if (expirationTime > callbackExpirationTime) {
// Existing callback has sufficient timeout. Exit.
return;
} else {
// 但是!如果传入的任务优先级更高,则要打断已经在调度的任务
if (callbackID !== null) {
// Existing callback has insufficient timeout. Cancel and schedule a
// new one.
cancelDeferredCallback(callbackID);
}
}
// The request callback timer is already running. Don't start a new one.
} else {
startRequestCallbackTimer(); // 涉及到开发工具和polyfill,略过
}
// 如果是取消了老的调度任务,或者是尚未有调度任务,则接下来会安排调度
callbackExpirationTime = expirationTime;
// 计算出任务的timeout,也就是距离此刻还有多久过期
const currentMs = now() - originalStartTimeMs; // originalStartTimeMs 代表react应用最初被加载的那一刻
const expirationTimeMs = expirationTimeToMs(expirationTime);
const timeout = expirationTimeMs - currentMs;
// 类似于 setTimeout 返回的 ID,可以用来延期回调
callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
}
6. unstable_scheduleCallback
前面都还在 packages/react-reconciler/ReactFiberScheduler.js 中,下面就要跟着刚才的 **scheduleDeferredCallback **辗转进入到单独的 packages/scheduler 包中:
- 根据不同优先级等级计算不同的 callbackNode 上的过期时间。
- 存储以过期时间为优先级的环形链表,用时可借助首节点
firstCallbackNode
可对链表进行遍历读取。 -
firstCallbackNode
变了后要调用ensureHostCallbackIsScheduled
重新遍历链表进行调度。
unstable_scheduleCallback:
function unstable_scheduleCallback(callback, deprecated_options) {
var startTime =
currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
var expirationTime;
if (
typeof deprecated_options === 'object' &&
deprecated_options !== null &&
typeof deprecated_options.timeout === 'number'
) {
// FIXME: Remove this branch once we lift expiration times out of React.
expirationTime = startTime + deprecated_options.timeout;
} else {
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
break;
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
}
}
var newNode = {
callback,
priorityLevel: currentPriorityLevel,
expirationTime,
next: null,
previous: null,
};
// Insert the new callback into the list, ordered first by expiration, then
// by insertion. So the new callback is inserted any other callback with
// equal expiration.
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled();
} else {
var next = null;
var node = firstCallbackNode;
do {
if (node.expirationTime > expirationTime) {
// The new callback expires before this one.
next = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);
if (next === null) {
// No callback with a later expiration was found, which means the new
// callback has the latest expiration in the list.
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
// The new callback has the earliest expiration in the entire list.
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled();
}
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}
return newNode;
}
7. ensureHostCallbackIsScheduled
- 该方法名字就说明了目的是保证 callback 会被调度,故若已经有 callbackNode 在被调度,自会自动循环。
- 从头结点,也就是最先过期的 callbackNode 开始请求调用,顺表如果有已存在的调用要取消。这就是之前说过的参与调用的任务有两种被打断的可能:1. 时间片到点了,2. 有更高优先级的任务参与了调度
function ensureHostCallbackIsScheduled() {
if (isExecutingCallback) {
// Don't schedule work yet; wait until the next time we yield.
return;
}
// Schedule the host callback using the earliest expiration in the list.
var expirationTime = firstCallbackNode.expirationTime;
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
} else {
// Cancel the existing host callback.
cancelHostCallback();
}
requestHostCallback(flushWork, expirationTime);
}
requestHostCallback = function(callback, absoluteTimeout) {
scheduledHostCallback = callback;
timeoutTime = absoluteTimeout;
// 超时了要立即安排调用
if (isFlushingHostCallback || absoluteTimeout < 0) {
// Don't wait for the next frame. Continue working ASAP, in a new event.
window.postMessage(messageKey, '*');
} else if (!isAnimationFrameScheduled) {
// 没有超时,就常规安排,等待时间片
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
};
// 取消之前安排的任务回调,就是重置一些变量
cancelHostCallback = function() {
scheduledHostCallback = null;
isMessageEventScheduled = false;
timeoutTime = -1;
};
为了模拟 requestIdleCallback
API:
传给 window.requestanimationframe
的回调函数会在浏览器下一次重绘之前执行,也就是执行该回调后浏览器下面会立即进入重绘。使用 window.postMessage
技巧将空闲工作推迟到重新绘制之后。
具体太过复杂,就大概听个响吧,若要深究则深究:
- animationTick
- idleTick
// 仅供示意
requestAnimationFrameWithTimeout(animationTick);
var animationTick = function(rafTime) {
requestAnimationFrameWithTimeout(animationTick);
}
window.addEventListener('message', idleTick, false);
window.postMessage(messageKey, '*');
react 这里还能统计判断出平台刷新频率,来动态减少 react 自身运行所占用的时间片,支持的上限是 120hz 的刷新率,即每帧总共的时间不能低于 8ms。
此间如果一帧的时间在执行 react js 之前就已经被浏览器用完,那么对于非过期任务,等待下次时间片;而对于过期任务,会强制执行。
8. flushWork
ensureHostCallbackIsScheduled
中的 requestHostCallback(flushWork, expirationTime)
参与时间片调度:
flushWork:
- 即使当前时间片已超时,也要把 callbackNode 链表中所有已经过期的任务先强制执行掉
- 若当前帧还有时间片,则常规处理任务
function flushWork(didTimeout) {
isExecutingCallback = true;
deadlineObject.didTimeout = didTimeout;
try {
// 把callbackNode链表中所有已经过期的任务先强制执行掉
if (didTimeout) {
// Flush all the expired callbacks without yielding.
while (firstCallbackNode !== null) {
// Read the current time. Flush all the callbacks that expire at or
// earlier than that time. Then read the current time again and repeat.
// This optimizes for as few performance.now calls as possible.
var currentTime = getCurrentTime();
if (firstCallbackNode.expirationTime <= currentTime) {
do {
flushFirstCallback();
} while (
firstCallbackNode !== null &&
firstCallbackNode.expirationTime <= currentTime
);
continue;
}
break;
}
} else {
// 当前帧还有时间片,则继续处理任务
// Keep flushing callbacks until we run out of time in the frame.
if (firstCallbackNode !== null) {
do {
flushFirstCallback();
} while (
firstCallbackNode !== null &&
getFrameDeadline() - getCurrentTime() > 0
);
}
}
} finally {
isExecutingCallback = false;
if (firstCallbackNode !== null) {
// There's still work remaining. Request another callback.
ensureHostCallbackIsScheduled();
} else {
isHostCallbackScheduled = false;
}
// Before exiting, flush all the immediate work that was scheduled.
flushImmediateWork();
}
}
flushFirstCallback
负责处理链表节点,然后执行 flushedNode.callback
。
9. performWork
- 是否有 deadline 的区分
- 循环渲染 Root 的条件
- 超过时间片的处理
performSyncWork 不会传 deadline。
没有deadline时,会循环执行 root 上的同步任务,或者任务过期了,也会立马执行任务。
performAsyncWork:
function performAsyncWork(dl) {
if (dl.didTimeout) { // 是否过期
if (firstScheduledRoot !== null) {
recomputeCurrentRendererTime();
let root: FiberRoot = firstScheduledRoot;
do {
didExpireAtExpirationTime(root, currentRendererTime);
// The root schedule is circular, so this is never null.
root = (root.nextScheduledRoot: any);
} while (root !== firstScheduledRoot);
}
}
performWork(NoWork, dl);
}
performSyncWork:
function performSyncWork() {
performWork(Sync, null);
}
performWork:
function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
deadline = dl;
// Keep working on roots until there's no more work, or until we reach
// the deadline.
findHighestPriorityRoot();
if (deadline !== null) {
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
if (enableUserTimingAPI) {
const didExpire = nextFlushedExpirationTime < currentRendererTime;
const timeout = expirationTimeToMs(nextFlushedExpirationTime);
stopRequestCallbackTimer(didExpire, timeout);
}
while (
nextFlushedRoot !== null &&
nextFlushedExpirationTime !== NoWork &&
(minExpirationTime === NoWork ||
minExpirationTime >= nextFlushedExpirationTime) &&
(!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)
) {
performWorkOnRoot(
nextFlushedRoot,
nextFlushedExpirationTime,
currentRendererTime >= nextFlushedExpirationTime,
);
findHighestPriorityRoot();
recomputeCurrentRendererTime();
currentSchedulerTime = currentRendererTime;
}
} else {
while (
nextFlushedRoot !== null &&
nextFlushedExpirationTime !== NoWork &&
(minExpirationTime === NoWork ||
minExpirationTime >= nextFlushedExpirationTime)
) {
performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
findHighestPriorityRoot();
}
}
// We're done flushing work. Either we ran out of time in this callback,
// or there's no more work left with sufficient priority.
// If we're inside a callback, set this to false since we just completed it.
if (deadline !== null) {
callbackExpirationTime = NoWork;
callbackID = null;
}
// If there's work left over, schedule a new callback.
if (nextFlushedExpirationTime !== NoWork) {
scheduleCallbackWithExpirationTime(
((nextFlushedRoot: any): FiberRoot),
nextFlushedExpirationTime,
);
}
// Clean-up.
deadline = null;
deadlineDidExpire = false;
finishRendering();
}
performWorkOnRoot:
- isRendering 标记现在开始渲染了
- 判断 finishedWork:是:调用 completeRoot 进入下一章的 commit 阶段;否:调用 renderRoot 遍历 Fiber 树。
function performWorkOnRoot(
root: FiberRoot,
expirationTime: ExpirationTime,
isExpired: boolean,
) {
isRendering = true;
// Check if this is async work or sync/expired work.
if (deadline === null || isExpired) {
// Flush work without yielding.
// TODO: Non-yieldy work does not necessarily imply expired work. A renderer
// may want to perform some work without yielding, but also without
// requiring the root to complete (by triggering placeholders).
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
completeRoot(root, finishedWork, expirationTime);
} else {
root.finishedWork = null;
// If this root previously suspended, clear its existing timeout, since
// we're about to try rendering again.
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
const isYieldy = false;
renderRoot(root, isYieldy, isExpired);
finishedWork = root.finishedWork;
if (finishedWork !== null) {
// We've completed the root. Commit it.
completeRoot(root, finishedWork, expirationTime);
}
}
} else {
// Flush async work.
let finishedWork = root.finishedWork;
if (finishedWork !== null) {
// This root is already complete. We can commit it.
completeRoot(root, finishedWork, expirationTime);
} else {
root.finishedWork = null;
// If this root previously suspended, clear its existing timeout, since
// we're about to try rendering again.
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
const isYieldy = true;
renderRoot(root, isYieldy, isExpired);
finishedWork = root.finishedWork;
if (finishedWork !== null) {
// We've completed the root. Check the deadline one more time
// before committing.
if (!shouldYield()) {
// Still time left. Commit the root.
completeRoot(root, finishedWork, expirationTime);
} else {
// There's no time left. Mark this root as complete. We'll come
// back and commit it later.
root.finishedWork = finishedWork;
}
}
}
}
isRendering = false;
}
10. renderRoot
- 调用 workLoop 进行循环单元更新
- 捕获错误并进行处理
- 走完流程之后善后
**renderRoot **流程:
- 遍历 Fiber 树的每个节点。
根据 Fiber 上的 updateQueue 是否有内容,决定是否要更新那个 Fiber 节点,并且计算出新的 state,
对于异步任务,更新每个 Fiber 节点时都要判断时间片是否过期,如果一个 Fiber 更新时出错,则其子节点就不用再更新了。最终整个 Fiber 树遍历完之后,根据捕获到的问题不同,再进行相应处理。
- createWorkInProgress:renderRoot 中,调用 createWorkInProgress 创建 “workInProgress” 树,在其上进行更新操作。在 renderRoot 开始之后,所有的操作都在 “workInProgress” 树上进行,而非直接操作 “current” 树。(双buff机制)
-
workLoop:开始更新一颗 Fiber 树上的每个节点,
isYieldy
指示是否可以中断,对于 sync 任务和已经超时的任务都是不可中断的,于是 while 循环更新即可;对于可中断的,则每次 while 循环条件中还要判断是否时间片到点需先退出。
function workLoop(isYieldy) {
if (!isYieldy) {
// Flush work without yielding
while (nextUnitOfWork !== null) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
} else {
// Flush asynchronous work until the deadline runs out of time.
while (nextUnitOfWork !== null && !shouldYield()) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
}
- performUnitOfWork:更新子树:
function performUnitOfWork(workInProgress: Fiber): Fiber | null {
const current = workInProgress.alternate;
// See if beginning this work spawns more work.
startWorkTimer(workInProgress);
let next;
if (enableProfilerTimer) {
if (workInProgress.mode & ProfileMode) {
startProfilerTimer(workInProgress);
}
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
if (workInProgress.mode & ProfileMode) {
// Record the render duration assuming we didn't bailout (or error).
stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
}
} else {
next = beginWork(current, workInProgress, nextRenderExpirationTime);
workInProgress.memoizedProps = workInProgress.pendingProps;
}
if (next === null) {
// If this doesn't spawn new work, complete the current work.
next = completeUnitOfWork(workInProgress);
}
ReactCurrentOwner.current = null;
return next;
}
- beginWork:开始具体的节点更新,下一章再说。
Root 节点具体怎么遍历更新,以及不同类型组件的更新,将在下一篇探讨。
image