创建更新之后,找到 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 操作。
其中红框中的步骤随时可能由于以下原因被中断:
- 有其他更高优任务需要先更新
- 当前帧没有剩余时间
由于红框中的工作都在内存中进行,不会更新页面上的DOM,即使反复中断用户也不会看见更新不完全的DOM。
因此可以说 Scheduler 和 Reconciler 是和平台无关的,而和平台相关的是 Renderer。
大致更新调度的流程:
- 首先通过 ReactDOM.render/setState/forceUpdate 产生更新。
- 找到产生更新的节点所对应的 FiberRoot,将其加入到调度器中(多次调用 ReactDOM.render 就会有多个 root)。
- 根据 expirationTime 判断是同步更新,还是异步更新。具体就是在上一篇提到的,在
computeExpirationForFiber()
计算过期时间时,根据fiber.mode & ConcurrentMode
模式是否开启,来计算同步的过期时间,和异步的过期时间,也就是同步更新任务
和异步更新任务
。同步更新任务没有 deadline(用于 react 执行的分片时间),会立即执行,不会被中断。而异步更新任务有 deadline,需要等待调度,并可能会中断。在此过程中,同步任务和异步任务最终会汇聚到一起,根据是否有 deadline,会进入同步循环条件或异步循环条件,而这个循环就是指遍历整棵 Fiber 树的每个节点进行更新的操作。同步任务遍历完更新完就完了,而异步任务在更新节点时会受到分片调度的控制。 - 异步更新任务被加入到 Scheduler 的 callbackList 中等待调度。调度时,会检查是否有任务已经过期(expirationTime),会先把所有已经超时的任务执行掉,直到遇到非超时的任务时,如果当前时间分片 deadline 还没到点,则继续执行,如果已经到点了,则控制权交给浏览器。
- 采用了 deadline 分片保证异步更新任务不会阻塞浏览器 GUI 的渲染、如动画等能在 30FPS 以上。
TODO:Scheduler 图,可能要自己手绘
2. scheduleWork
在上一篇 “React 中的更新” 提到,ReactDOM.render/setState/forceUpdate 最终都会进入 scheduleWork 即调度工作。
- 找到更新所对应的 FiberRoot 节点。
- 如果符合条件则重置 stack
- 如果符合条件就请求工作调度
TODO: Fiber 树图
**
点击 button,是 List 组件实例调用了 setState,创建 update 后开始调度时,是将 List 所在的 RootFiber 这个 fiber 根节点加入到调度队列中(而并不是直接把 List Fiber 节点加入调度中),正如每次更新时也是从 RootFiber 开始更新。
一些全局变量:
- isWorking: 用来标志是否当前有更新正在进行,不区分阶段,包含了 commit 和 render 阶段
- isCommitting: 是否处于 commit 阶段
React在16版本之后处理任务分为两个阶段:
- render 阶段: 判断哪些变更需要被处理成 DOM,也就是比较上一次渲染的结果和新的更新,打标记。
- commit 阶段: 处理从 js 对象到 DOM 的更新,不会被打断,并且会调用 componentDidMount 和 componentDidUpdate 这些生命周期方法。
function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
const root = scheduleWorkToRoot(fiber, expirationTime);
if (root === null) {
return;
}
// 异步任务由于时间片不够被中断,执行权了交给浏览器,
// 此时产生了更新任务,其优先级更高,则会打断老的任务。
if (
!isWorking &&
nextRenderExpirationTime !== NoWork &&
expirationTime < nextRenderExpirationTime
) {
// This is an interruption. (Used for performance tracking.)
interruptedBy = fiber; // 给开发工具用的,用来展示被哪个节点打断了异步任务
resetStack(); // 重置之前的中断的任务已经产生的部分节点更新
}
// 暂时先忽略
markPendingPriorityLevel(root, expirationTime);
// 没有正在进行的工作,或者是上一次render阶段已经结束(更新结束),已经到了commit阶段,
// 那么可以继续请求一次调度工作。
if (
// If we're in the render phase, we don't need to schedule this root
// for an update, because we'll do it before we exit...
!isWorking ||
isCommitting ||
// ...unless this is a different root than the one we're rendering.
nextRoot !== root // 单个FiberRoot入口时nextRoot永远等于root,所以基本不需要考虑这个条件,因为基本都是单入口应用
) {
const rootExpirationTime = root.expirationTime;
requestWork(root, rootExpirationTime); // 请看下一节
}
// 检测死循环更新
if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {
// Reset this back to zero so subsequent updates don't throw.
nestedUpdateCount = 0;
invariant(
false,
'Maximum update depth exceeded. This can happen when a ' +
'component repeatedly calls setState inside ' +
'componentWillUpdate or componentDidUpdate. React limits ' +
'the number of nested updates to prevent infinite loops.',
);
}
}
function resetStack() {
// 用于记录render阶段Fiber树遍历过程中下一个需要执行的节点,
// 不为null说明之前存在更新任务,只是时间片不够了,被中断了
// 于是就要向上遍历父节点,将已经更新了的状态回退到更新之前,
// 回退是为了避免状态混乱,不能把搞了一半的摊子直接丢给新的更新任务,
// 因为新的高优先级任务也要从根节点RootFiber开始更新。
if (nextUnitOfWork !== null) {
let interruptedWork = nextUnitOfWork.return;
while (interruptedWork !== null) {
unwindInterruptedWork(interruptedWork);
interruptedWork = interruptedWork.return;
}
}
if (__DEV__) {
ReactStrictModeWarnings.discardPendingWarnings();
checkThatStackIsEmpty();
}
// 回退一些全局变量
nextRoot = null;
nextRenderExpirationTime = NoWork;
nextLatestAbsoluteTimeoutMs = -1;
nextRenderDidError = false;
nextUnitOfWork = null;
}
scheduleWorkToRoot()
根据传入的 Fiber 节点,找到对应的 FiberRoot(也就是最初调用 ReactDOM.render 时创建的 FiberRoot)。对于 ReactDOM.render 产生的调用 scheduleWork()
,其传入的是 RootFiber 节点, RootFiber.stateNode
就找到了 FiberRoot;而 setState/forceUpdate 传入的是自身组件所对应的 Fiber 子节点,会复杂一些:
function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
// 记录调度时间,
recordScheduleUpdate();
// 如果尚未更新过,没有存储过期时间,或者最新计算出的过期时间更短,意味着优先级更高,
// 那么就更新目标Fiber上的过期时间 (即产生更新的那个Fiber,如刚才的List组件对应的Fiber)
// Update the source fiber's expiration time
if (
fiber.expirationTime === NoWork ||
fiber.expirationTime > expirationTime
) {
fiber.expirationTime = expirationTime;
}
let alternate = fiber.alternate;
// 同理如果alternate fiber(之前说过的双缓存、双buff机制)存在,也要尝试更新过期时间
if (
alternate !== null &&
(alternate.expirationTime === NoWork ||
alternate.expirationTime > expirationTime)
) {
alternate.expirationTime = expirationTime;
}
// Walk the parent path to the root and update the child expiration time.
let node = fiber.return; // fiber.return指向的就是父fiber节点。
let root = null;
// 只有RootFiber.return会是null,说明传入的Fiber就是RootFiber,其tag就是3,也就是HostRoot
if (node === null && fiber.tag === HostRoot) {
root = fiber.stateNode; // 找到了 FiberRoot
} else {
// 向上遍历,寻找FiberRoot
while (node !== null) {
alternate = node.alternate;
if (
node.childExpirationTime === NoWork ||
node.childExpirationTime > expirationTime
) {
// childExpirationTime: 父节点记录了子树中优先级最高的过期时间,即最先的过期时间
// 如果当前传入节点的过期时间优先级更高,则更新父节点的childExpirationTime,
// 同理还要更新父节点的alternate节点。
node.childExpirationTime = expirationTime;
if (
alternate !== null &&
(alternate.childExpirationTime === NoWork ||
alternate.childExpirationTime > expirationTime)
) {
alternate.childExpirationTime = expirationTime;
}
// node.childExpirationTime不需要更新,node.alternate.childExpirationTime也要尝试更新
} else if (
alternate !== null &&
(alternate.childExpirationTime === NoWork ||
alternate.childExpirationTime > expirationTime)
) {
alternate.childExpirationTime = expirationTime;
}
// 找到了 FiberRoot 就跳出
if (node.return === null && node.tag === HostRoot) {
root = node.stateNode;
break;
}
// 没找到则指针上溯,继续下次while循环
node = node.return;
}
}
// 提醒当前传入的fiber没有找到FiberRoot节点
if (root === null) {
if (__DEV__ && fiber.tag === ClassComponent) {
warnAboutUpdateOnUnmounted(fiber);
}
return null;
}
// 跟踪应用更新的相关代码,精力和水平有限,汪洋大海不再深究。
if (enableSchedulerTracing) {
const interactions = __interactionsRef.current;
if (interactions.size > 0) {
const pendingInteractionMap = root.pendingInteractionMap;
const pendingInteractions = pendingInteractionMap.get(expirationTime);
if (pendingInteractions != null) {
interactions.forEach(interaction => {
if (!pendingInteractions.has(interaction)) {
// Update the pending async work count for previously unscheduled interaction.
interaction.__count++;
}
pendingInteractions.add(interaction);
});
} else {
pendingInteractionMap.set(expirationTime, new Set(interactions));
// Update the pending async work count for the current interactions.
interactions.forEach(interaction => {
interaction.__count++;
});
}
const subscriber = __subscriberRef.current;
if (subscriber !== null) {
const threadID = computeThreadID(
expirationTime,
root.interactionThreadID,
);
subscriber.onWorkScheduled(interactions, threadID);
}
}
}
return root;
}
3. requestWork
- 加入到 root 调度队列
- 判断是否批量更新
- 根据 expirationTime 判断调度类型
requestWork:
// requestWork is called by the scheduler whenever a root receives an update.
// It's up to the renderer to call renderRoot at some point in the future.
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
addRootToSchedule(root, expirationTime);
if (isRendering) {
// Prevent reentrancy. Remaining work will be scheduled at the end of
// the currently rendering batch.
return;
}
// 关于批处理,下一小节来探讨
if (isBatchingUpdates) {
// Flush work at the end of the batch.
if (isUnbatchingUpdates) {
// ...unless we're inside unbatchedUpdates, in which case we should
// flush it now.
nextFlushedRoot = root;
nextFlushedExpirationTime = Sync;
performWorkOnRoot(root, Sync, true);
}
return;
}
// TODO: Get rid of Sync and use current time?
if (expirationTime === Sync) {
// 同步调用js代码直到结束为止,不会被打断。
performSyncWork();
} else {
// 否则进行异步调度,进入到了独立的scheduler包中, 即react的requestIdleCallback polyfill
// 等待有在deadline时间片内才能得到执行,deadline到点后则控制权交给浏览器,等待下一个时间片。
scheduleCallbackWithExpirationTime(root, expirationTime);
}
}
addRootToSchedule:
- 检查 root 是否已参与调度,没有则加入到 root 调度队列,实际就是单向链表添加节点的操作。当然绝大多数时候,我们的应用只有一个 root。
- 如果 root 已经参与调度,但可能需要提升优先级,使用 expirationTime 保存最高优先级的任务。
function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
// Add the root to the schedule.
// Check if this root is already part of the schedule.
if (root.nextScheduledRoot === null) {
// This root is not already scheduled. Add it.
root.expirationTime = expirationTime;
if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
root.nextScheduledRoot = root;
} else {
lastScheduledRoot.nextScheduledRoot = root;
lastScheduledRoot = root;
lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
}
} else {
// This root is already scheduled, but its priority may have increased.
const remainingExpirationTime = root.expirationTime;
if (
remainingExpirationTime === NoWork ||
expirationTime < remainingExpirationTime
) {
// Update the priority.
root.expirationTime = expirationTime;
}
}
}
4. batchUpdates
顾名思义,批量更新,可以避免短期内的多次渲染,攒为一次性更新。
在后面提供的 demo 中的 handleClick
中有三种方式调用 this.countNumber()
。
**
第1种:
批量更新,会打印 0 0 0,然后按钮文本显示为3。每次 setState
虽然都会经过 enqueueUpdate
(创建update 并加入队列)-> scheduleWork
(寻找对应的 FiberRoot 节点)-> requestWork
(把 FiberRoot 加入到调度队列),可惜上下文变量 isBatchingUpdates
在外部某个地方被标记为了 true
,因此本次 setState
一路走来,尚未到达接下来的 performSyncWork
或者 scheduleCakkbackWithExpirationTime
就开始一路 return 出栈:
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
的回调执行的时候,专门再创建一个批量更新上下文即可:
**
所以,setState 是同步还是异步?
**
setState 方法本身是被同步调用,但并不代表 react 的 state 就会被立马同步地更新,而是要根据当前执行上下文来判断。
如果处于批量更新的情况下,state 不会立马被更新,而是批量更新。
如果非批量更新的情况下,那么就“有可能”是立马同步更新的。为什么不是“一定”?因为如果 React 开启了 Concurrent Mode,非批量更新会进入之前介绍过的异步调度中(时间分片)。
批量更新演示 demo:
import React from 'react'
import { unstable_batchedUpdates as batchedUpdates } from 'react-dom'
export default class BatchedDemo extends React.Component {
state = {
number: 0,
}
handleClick = () => {
// 事件处理函数自带batchedUpdates
// this.countNumber()
// setTimeout中没有batchedUpdates
setTimeout(() => {
this.countNumber()
}, 0)
// 主动batchedUpdates
// setTimeout(() => {
// batchedUpdates(() => this.countNumber())
// }, 0)
}
countNumber() {
const button = document.getElementById('myButton')
const num = this.state.number
this.setState({
number: num + 1,
})
console.log(this.state.number)
console.log(button.innerText)
this.setState({
number: num + 2,
})
console.log(this.state.number)
console.log(button.innerText)
this.setState({
number: num + 3,
})
console.log(this.state.number)
console.log(button.innerText)
}
render() {
return
}
}
5. Scheduler 调度器
- 维护时间片
- 模拟 requestIdleCallback
- 调度列表和超时判断
如果1秒30帧,那么需要是平均的30帧,而不是前0.5秒1帧,后0.5秒29帧,这样也会感觉卡顿的。
Scheduler 目的就是保证 React 执行更新的时间,在浏览器的每一帧里不超过一定值。
不要过多占用浏览器用来渲染动画或者响应用户输入的处理时间。
**
继续之前的源码,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 节点具体怎么遍历更新,以及不同类型组件的更新,将在下一篇探讨。