原文地址
1. 开始
让我们以下面这段代码开始,这部分我们在这系列的第一季已经讲过了。
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
换句话说,React内部工作在fiber树的那个fiber上, workInProgress是跟踪当前位置,遍历算法在我前面的文章里已经讲过了。
workLoopSync()是非常容易去理解的,因为它是同步的,它不会中断我们的进程,所以React会继续在一个循环中工作。
在并发模式下存在一些差异
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
在并发模式下,有更高的权利的任务可以中止低权利的任务,我们需要一种方法去中断和恢复任务,这就是为什么shouldYield()获得了成功,但显然不止于此。
2.让我们先从一些背景知识开始
2.1 事件循环
老实说,我不能够解释的很好,我建议你读来自javascript.info的解释,观看这个视频。
简单来说,JavaScript引擎将会做下面的事情:
1、从任务队列中获取任务(宏任务)并运行
2、如果是微任务,运行它们
3、 4、如果有更多任务,请重复1或等待更多任务
这个循环是不言自明的,因为这里确实有一个循环
2.2 setimmediation()在不阻塞呈现的情况下调度一个新任务
为了在不阻塞的情况下调度任务,我们已经熟悉了setTimeout(callback, 0)的技巧,it schedules a new macrotask
但是有一个更好的API setimmediation(),但它只在IE和node.js中可用
它更好,因为setTimeout()实际上在嵌套调用中至少有大约4ms, setimmediation()没有延迟
好的,我们已经准备好接触React Scheduler源代码中的第一段代码
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === "function") {
// Node.js and old IE.
// 有几个原因可以解释为什么我们更喜欢setimmediation.
//
// 与MessageChannel不同,它不会阻止Node.js进程退出
// (尽管这是调度器的DOM分支,但您可以混合使用Node.js 15+,其中有一个MessageChannel和 // jsdom)
// https://github.com/facebook/react/issues/20756
//
// 而且,它运行得更早,这是我们想要的
// 如果其他浏览器实现了它,最好使用它
// 尽管这两种方法都不如本机调度
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== "undefined") {
// DOM和线程的环境
// 我更喜欢MessageChannel,因为set Timeout有4ms的间隔
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
// We should only fallback here in non-browser environments.
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
这里我们可以看到setimmediation()的两种不同回退,分别是MessageChannel和setTimeout
2.3 优先队列
优先队列是一个在调度中常用的数据结构。我建议你尝试自己用JS创建一个优先队列
它非常适合React中的需求。因为有不同优先级的事件发生,我们得尽快找到一个优先级最高的
React使用minheap实现优先队列,你可以在这里找到源代码
3. 调用workLoopConcurrent堆栈
现在,让我们看看如何调用workLoopConcurrent
所有的代码都在ReactFiberWorkLoop.js中,让我们分解一下
我们遇到过很多次ensure erootisscheduled(),它从相当多的地方被使用,顾名思义,如果有任何更新,ensure erootisscheduled()为React安排一个任务
注意,它不会直接调用performConcurrentWorkOnRoot(),而是通过scheduleCallback(优先级,回调)将其视为回调
scheduleCallback()是Scheduler中的一个api
我们将很快深入讨论调度器,但是现在,请记住,调度器将在正确的时间运行任务
3.1 如果中断,performConcurrentWorkOnRoot()返回自身的闭包
请参见performConcurrentWorkOnRoot()根据进度返回不同的结果
如果shouldyfield()为true, workLoopConcurrent将中断,从而导致不完整的更新(RootInComplete), performConcurrentWorkOnRoot()将返回performConcurrentWorkOnRoot.bind(null, root)
如果是complete,则返回null
您可能想知道,如果某个任务被shouldyfield()中断,它将如何恢复?是的,这就是答案。调度器会查看任务回调的返回值,返回值是一种重新调度
我们将很快讨论这个问题
4.调度器
最后,我们进入了调度器的世界。不要害怕,一开始我很害怕,但很快意识到我不需要害怕
消息队列是处理外部控制的一种方式,调度器就是这样做的
上面提到的scheduleCallback()在Scheduler世界中是不稳定的scheduleCallback
4.1 scheduleCallback() -调度器通过exipriationTime调度任务
为了让Scheduler调度任务,它首先需要存储带有优先级标记的任务
这是由优先队列完成的,我们已经在背景知识中介绍过了
它使用expirationTime来反映优先级。这很公平,越早到期,我们就越早处理。下面是在scheduleCallback()中创建任务的代码
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 LowPriority:
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,
};
代码非常简单,对于每个优先级我们都有不同的超时,它们在这里定义
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
所以默认它有5秒的超时,对于用户阻塞它有250ms。我们将很快看到这些优先事项的一些例子
任务已经创建,现在是时候将其放入优先队列中了
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 (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// 如果需要,计划一个主机回调。如果我们已经在工作了,那就等到下次我们yield的时候再做
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
哦,对了,当调度一个任务时,它可以有一个延迟选项,比如setTimeout()。让我们先把它放在一边,以后再来
只需要关注else
分支。我们可以看到两个重要的调用
1、push(taskQueue, newTask);
将任务添加到队列中,这只是优先级队列API,我跳过
2、requestHostCallback(flushWork)
处理它们
requestHostCallback(flushWork)是必要的,因为调度器是主机无关的,它应该只是一些独立的黑盒子,可以在任何主机上运行,所以它需要被请求
4.2 requestHostCallback()
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// 跟踪开始时间,这样我们就可以测量主线程被阻塞了多长时间
startTime = currentTime;
const hasTimeRemaining = true;
// 如果调度程序任务抛出,则退出当前浏览器任务,以便可以观察到错误
//
// 有意地不使用try-catch,因为这会使一些调试技术更加困难。相反,如果' scheduledHostCallback '错误,那么' hasMoreWork '将保持true,我们将继续工作循环
let hasMoreWork = true;
try {
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
if (hasMoreWork) {
// 如果有更多的工作,则将下一个消息事件安排在前一个消息事件的末尾
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
}
// 浏览器将给它一个绘制的机会,所以我们可以重置这个。
needsPaint = false;
};
在2.2中提到的schedulePerformWorkUntilDeadline()
只是performWorkUntilDeadline()
的一个包装
scheduledHostCallback
设置在requestHostCallback()
并且在performWorkUntilDeadline()
中马上调用,这是为了给主线程渲染的机会
忽略一些细节,这是最重要的一行,hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime)
,它意味着flushWork()
将被(true, currentTime)
调用
4.3 flushWork()
try {
// 在生产代码中没有捕获
return workLoop(hasTimeRemaining, initialTime);
} finally {
//
}
flushWork刚刚结束wraps up workLoop()
4.4 workLoop() - the core of Scheduler
正如workLoopConcurrent()
在协调中,workLoop()
是调度器的核心。它们有相似的名字是因为它们有相似的进程。
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// 这个currentTime没有过期, 我们到达了死亡线
break;
}
就像workLoopConcurrent()
一样,shouldYieldToHost()
在这里被选中。我们稍后再讨论。
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.callback
,在这个例子中实际上是performConcurrentWorkOnRoot()
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didUserCallbackTimeout);
它调用时带有一个标志,以指示它是否过期
如果超时,performConcurrentWorkOnRoot()
将退回到同步模式
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
好的,回到workLoop()
if (typeof continuationCallback === "function") {
currentTask.callback = continuationCallback;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
重要的是,我们可以看到只有当callback的返回值不是function时任务才会弹出,如果它是函数,它只会更新任务的回调,因为它没有弹出,workLoop()的下一次tick将再次导致相同的任务
这意味着如果这个回调的返回值是一个函数,这意味着这个任务还没有完成,我们应该重新处理它,这里的点连接到3.2
advanceTimers(currentTime);
这是针对延迟任务的,我们稍后再回来
4.5 how shouldYield()
work?#
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
// 主线程只被阻塞了很短的时间
// 比单帧小
return false;
}
// 主线程阻塞了相当长的时间。我们可能希望放弃主线程的控制权,这样浏览器就可以执行高优先级
// 的任务 主要是绘图和用户输入。如果有一个未决的绘制或未决的输入,那么我们应该让步。 但如
// 果两者都没有,那么我们可以在保持响应的同时减少让步。 不管怎样,我们最终还是会让步,因为
// 可能会有一个挂起的绘制没有伴随着对“requestPaint”的调用,或者其他主线程任务,比如网络
//事件
if (enableIsInputPending) {
if (needsPaint) {
// 有一个挂起的绘制(由' requestPaint '发出信号),现在让步
return true;
}
if (timeElapsed < continuousInputInterval) {
// 我们还没有阻塞线程那么久。只有在等待离散输入(例如点击)时才让步。
// 如果有未决的连续输入(例如鼠标悬停),这是可以的。
if (isInputPending !== null) {
return isInputPending();
}
} else if (timeElapsed < maxInterval) {
// 如果有一个悬而未决的离散或连续输入,Yield
if (isInputPending !== null) {
return isInputPending(continuousOptions);
}
} else {
// 我们已经阻塞线程很长时间了。即使没有待处理的输入,也可能有一些我们不知道的
// 其他预定工作,比如网络事件。现在Yield
return true;
}
}
// `isInputPending` 不是可用的。 现在Yield
return true;
}
其实并不复杂,评论解释了一切。基本线条如下所示
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
// 主线程只被阻塞了很短的时间;比单帧小的。不要Yield
return false;
}
return true;
因此,每个任务都有5ms (frameInterval),如果通过它,那么应该Yield
注意,这是用于在Scheduler
中运行任务,而不是用于每个performUnitOfWork()
,我们可以看到startTime
仅在performWorkUntilDeadline()
中设置,这意味着它将为每个flushWork()
重置,这意味着如果多个任务可以在flushWork()
中处理,则在两者之间没有yield
5 Summary
这真是太多了。让我们画一个总体图
虽然还有一些不足,但我们已经取得了巨大的进步。这已经是一个很大的图表,让我们把其他的东西放在下一集,包括
- 在调度器中延迟任务
- 如何确定优先级和例子