上一篇,我们介绍了commit阶段,其中一阶段触发了异步flushPassiveEffects执行。在react中,schedule是实现异步调度的核心。
react fiber架构,强调的是异步可中断渲染。为了提升react在大规模DOM场景下的性能,需要在浏览器空闲时执行一些任务,并且期望异步执行不影响DOM正常渲染。
requestIdleCallback是浏览器的一个api,在浏览器空闲时的回调函数。但react最终没有使用他。有两点原因:
-
- requestIdleCallback并非所有浏览器都支持,各浏览器兼容性也不达标
-
- requestIdleCallback的FPS只有20,达不到60FPS
requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it's not really useful for UI work。—— from Releasing Suspense
react自己实现了一套requestIdleCallback,schedule不依赖react,是一个单独的模块,也可以给其他框架使用。下面,我们来一探究竟。
一. scheduleCallback
function scheduleCallback(priorityLevel, callback, options) {
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;
}
// ...timeout
// ...newTask
// ...requestHostCallback
}
currentTime是当前精确到毫秒级的时间戳,其实在react17中,options都是null,即开始时间是当前时间戳。
timeout
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 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
双队列
在调度体系中,存在2个队列,一个是任务队列,一个已超时队列。
var newTask = {
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1
};
if (startTime > currentTime) {
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);
{
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
taskQueue是schedule需要调度的任务,是一个小顶堆数组。这里需要注意的是二叉堆排序,react利用堆排序降低算法复杂度。
function push(heap, node) {
var index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
function peek(heap) {
var first = heap[0];
return first === undefined ? null : first;
}
function pop(heap) {
var first = heap[0];
if (first !== undefined) {
var last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0);
}
return first;
} else {
return null;
}
}
function siftUp(heap, node, i) {
var index = i;
while (true) {
var parentIndex = index - 1 >>> 1;
var parent = heap[parentIndex];
if (parent !== undefined && compare(parent, node) > 0) {
// The parent is larger. Swap positions.
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
// The parent is smaller. Exit.
return;
}
}
}
function siftDown(heap, node, i) {
var index = i;
var length = heap.length;
while (index < length) {
var leftIndex = (index + 1) * 2 - 1;
var left = heap[leftIndex];
var rightIndex = leftIndex + 1;
var right = heap[rightIndex]; // If the left or right node is smaller, swap with the smaller of those.
if (left !== undefined && compare(left, node) < 0) {
if (right !== undefined && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (right !== undefined && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
// Neither child is smaller. Exit.
return;
}
}
}
function compare(a, b) {
// Compare sort index first, then task id.
var diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
timerQueue是超时的任务队列,但这是一个保留功能,并没有哪个地方调用。
二. requestHostCallback
requestHostCallback = function (callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
react会判断window是否存在,以及window上是否有MessageChannel,如果有才进入这段代码定义。否则使用setTimeout。
需要注意的是,react使用MessageChannel宏任务做异步处理task,为什么不使用setTimeout或者微任务promise呢?
这里待说到浏览器的事件循环,宏任务是DOM更新之后执行的,当然也并不是每次的事件循环都伴随着DOM更新。 为了不影响DOM的更新,react将耗时任务放到之后,也就是宏任务执行。 但每帧留给react的时间不长(5毫秒),setTimeout这样的api真正执行的时间也不稳定。
宏任务的处理函数将进入performWorkUntilDeadline。
三. performWorkUntilDeadline
var performWorkUntilDeadline = function () {
if (scheduledHostCallback !== null) {
var currentTime = getCurrentTime();
deadline = currentTime + yieldInterval;
var hasTimeRemaining = true;
try {
var hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
port.postMessage(null);
}
} catch (error) {
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
};
yieldInterval初始值为5毫秒,唯一能改变yieldInterval值的是forceFrameRate函数,但这也只是保留函数,全局并没有任何地方调用。这里每次宏任务执行时,定义了这个宏任务的deadline时间,其实了解requestIdeCallback api的同学会发现,react其实就是在模仿他的实现。ric里面也是可以拿到deadline的,有个注意点是,在浏览器长时间没有任务时,原生requestIdeCallback的deadline能延长到50毫秒。当然,这一切都是变化的。
hasMoreWork意味着,如果taskQueue还有任务,将继续发起宏任务调度。这也是中断再次恢复的关键。
四. flushWork
function flushWork(hasTimeRemaining, initialTime) {
// ...
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
var previousPriorityLevel = currentPriorityLevel;
try {
if (enableProfiling) {
try {
return workLoop(hasTimeRemaining, initialTime);
} catch (error) {
if (currentTask !== null) {
var currentTime = getCurrentTime();
markTaskErrored(currentTask, currentTime);
currentTask.isQueued = false;
}
throw error;
}
} else {
// No catch in prod code path.
return workLoop(hasTimeRemaining, initialTime);
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
{
var _currentTime = getCurrentTime();
markSchedulerSuspended(_currentTime);
}
}
}
isHostTimeoutScheduled是存在超时任务调度,但任务构建阶段,delay是不存在的,17版本未调用delay。即不存在timeout task。不过如果存在超时的时候,在task设计中,已超时5s,那么这样的任务,react认为需要cancel掉。
workLoop
function workLoop(hasTimeRemaining, initialTime) {
var currentTime = initialTime;
currentTask = peek(taskQueue);
while (currentTask !== null && !(enableSchedulerDebugging )) {
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) {
break;
}
var callback = currentTask.callback;
// ...
var continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
// ...
} else {
// ...
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
// ...
}
if (currentTask !== null) {
return true;
} else {
var firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
遍历taskQueue队列,如果存在:task未超时,但没有了时间切片或超过宏任务的deadline,那么直接终止循环。待下个事件循环中再执行。
其实,所谓的时间切片,就是在浏览器的每帧渲染16.6ms中(正常浏览器1秒60帧)中,react定义了5ms的时间片。即在每帧的5ms内,浏览器需要执行react相关的js代码,当超过5ms时,react终止一切任务,将执行权交还给浏览器执行DOM渲染或事件响应。 保证浏览器能够1秒刷新60下,而不被js占用导致帧率下降页面卡顿。
如果被中断的task队列,将返回isMoreTask true。将进入下一轮事件循环。
正如以上所述,schedule调度最高优先级是ImmediatePriority,其实他是个同步任务,不参与宏任务队列。直接执行了,比如我们的commit阶段,直接最高优先级插入DOM。
如果taskQueue结束后,依旧存在很久的超时任务,将使用setTimeout来执行,回调的函数将执行handleTimeout函数,最终还是会进入taskQueue的flushWork阶段,在进入workLoop执行。
五. 总结
时间切片原理
消费任务队列的过程中, 可以消费1~n
个 task, 甚至清空整个 queue. 但是在每一次具体执行task.callback
之前都要进行超时检测, 如果超时可以立即退出循环并等待下一次调用.
可中断渲染原理
在时间切片的基础之上, 如果单个task.callback
执行时间就很长, 就需要task.callback
自己能够检测是否超时, 所以在 fiber 树构造过程中, 每构造完成一个单元, 都会检测一次超时, 如遇超时就退出fiber树构造循环
, 并返回一个新的回调函数(就是此处的continuationCallback
)并等待下一次回调继续未完成的fiber树构造
。当然最后的effect task(如useEffect)也是如此。