引言
在React16版本之前,React架构是分为两部分的:Reconcile 和 Render。假设我们现在有一个很大的项目,我们在比较深的子组件里触发了更新,那么 v16 之前的版本是会全量进行递归更新的,假设递归更新的时间超过了 50ms,用户就会感觉到明显的卡顿。v16 的诞生就是为了解决这个问题,React 团队重构了架构,新增了一个 Schedule 的角色,这个角色可以进行任务的调度,让项目有了更高的性能。这篇文章主要是讲一下React 中 Schedule 的作用。
原理
这里放一张别人画好的图吧,出处我会在文末粘出来。
这张图就可以很形象的表示整个工作过程,由于 Schedule 比较复杂,本次讲解只讲解涉及到渲染方面的内容。
ensureRootIsScheduled
入口函数是ensureRootIsScheduled,我们来看一下这部分具体做了什么
/...
newCallbackNode = scheduleCallback$1(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
有些代码略过吧,如果React 版本是 18 的话,会进入一个scheduleCallback$1开启调度。
scheduleCallback$1
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = exports.unstable_now();
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: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1
};
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
} // Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
生成expirationTime
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;
此部分会根据传入的任务优先级去生成一个expirationTime,expirationTime很重要!!!后面 React 会根据这个属性判断任务是否过期,从而解决饥饿问题。
开启任务调度
var newTask = {
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1
};
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
} // Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
- 首先根据 startTime 和 currentTime 进行比较,如果开始时间大于当前时间,说明任务属于延迟任务,会 push 进入timerQueue,反之会 push 进入 taskQueue。
- 这里 push 方法其实维护了一个小顶堆,小顶堆的好处就是每次取出任务的时候,都是优先级最高的任务。
- 执行requestHostCallback 开启任务的调度requestHostCallback(flushWork);
任务调度
function requestHostCallback(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
schedulePerformWorkUntilDeadline = function () {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = function () {
port.postMessage(null);
};
} else {
// We should only fallback here in non-browser environments.
schedulePerformWorkUntilDeadline = function () {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
这里可以看到主要是执行了一个schedulePerformWorkUntilDeadline,而schedulePerformWorkUntilDeadline函数根据不同的运行时会调用不同的任务调度方法,方便兼容不同的浏览器。
我们主要分析主流浏览器环境即可。可以看到他使用 MessageChannel 开启了一个宏任务,那么这里就跟事件循环有关系了。如果有同学对 MessageChannel 不熟悉的,可以访问这个链接看看这篇文章:developer.mozilla.org/zh-CN/docs/…. 当浏览器有空闲状态后,就会执行我们启动的这次宏任务。
performWorkUntilDeadline
performWorkUntilDeadline这个函数就是 React 注册的,接收宏任务的函数,通过这个函数来执行具体的任务调度。
/...
try {
// workLoop返回的flag, 如果 workLoop 返回一个 true,会在开启一个调度者,在下一轮事件循环进行调度。
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
if (hasMoreWork) {
// If there's more work, schedule the next message event at the end
// of the preceding one.
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
可以看到具体的逻辑就是调用scheduledHostCallback来执行任务,同时这里有一个可恢复的逻辑,如果当前scheduledHostCallback返回了一个 true,那么会再次开启新一轮的宏任务,在下一轮的事件循环去继续处理任务。
scheduledHostCallback
try {
return workLoop(hasTimeRemaining, initialTime);
} catch (error) {
if (currentTask !== null) {
var currentTime = exports.unstable_now();
markTaskErrored(currentTask, currentTime);
currentTask.isQueued = false;
}
throw error;
}
内部会调用一个flushWork,flushWork 里的主要逻辑就是我上面粘贴的函数体,可以看到主要逻辑就是调用了一个 workLoop。 那么任务是否可恢复,与workLoop 的返回值是息息相关的。
workLoop
function workLoop(hasTimeRemaining, initialTime) {
var currentTime = initialTime;
advanceTimers(currentTime);
// 取出任务的最高优先级
currentTask = peek(taskQueue);
console.log('currentTask==',currentTask)
while (currentTask !== null && !(enableSchedulerDebugging )) {
// 判断当前任务是否过期
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
var callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
var didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 执行真正的调度任务,如果返回值是一个函数,继续执行 | 退出循环,下一轮调度继续执行。
var continuationCallback = callback(didUserCallbackTimeout);
currentTime = exports.unstable_now();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
} // Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
var firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
- 取出当前优先级最高的任务。
- 判断当前任务是否可以继续执行(任务是否过期 | hasTimeRemaining为 true,执行时间是否超过了 50ms)
- 通过 callback 进入 beginWork 和 completeWork 阶段
- 如果 callback 返回值是一个函数,说明当前任务没执行完毕,在下一轮事件循环还会继续执行。
- 所有任务执行完毕之后退出循环,结束调度。
下面来分析一下callback 具体干了什么
callback
callback 会被赋值为performConcurrentWorkOnRoot,我们可以看一下performConcurrentWorkOnRoot的实现逻辑
/...
var shouldTimeSlice = !includesBlockingLane(root, lanes) && !includesExpiredLane(root, lanes) && ( !didTimeout);
/ ...
var exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
/*KaSong*/logHook('continuationCallback', root);
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
主要逻辑就是根据shouldTimeSlice判断调用renderRootConcurrent还是renderRootSync。这两个方法的区别是一个可以被打断, 一个不可以被打断。
最后会根据callbackNode判断是否需要继续调度还是结束调度。
下面我们选一个来讲,renderRootConcurrent
renderRootConcurrent
do {
try {
workLoopConcurrent();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
主要逻辑就是进入 workLoopConcurrent以及判断workInProgress和shouldYield来决定是否需要继续调度。 shouldYield就是之前那个方法,判断当前执行时间是否超过了 50ms。
总结
整个过程基本上分析的差不多了,其实就是那张大图上所展示的,有两个 workLoop,一个大的 workLoop 用于判断当前任务是否需要继续调度还是结束调度。小的 workLoop 是用来判断单个 fiberNode 节点生成是否时间过长,如果时间过长,React 会结束这次渲染,在下一次事件循环继续开启任务来执行。