React Fiber调度算法笔记
React Fiber调度算法笔记
React 从v16开始引入了Fiber,为了支持时间分片,优先调度,支持渲染过程中中断,恢复。中断是为了让浏览器有机会响应更高优先级的事件。刚看到这个功能的时候就开始猜想,js是单线程的,要实现这个功能,React的渲染过程得自己将任务分片,每次执行一个分片,然后将下一个分片,塞入到宏任务队列。这样浏览器才有机会介入到渲染过程中,实现中断,恢复。出于好奇,就去翻阅了React的源码,大体的思路是一样的,但是它封装比较精致。通过阅读代码了解了部分的实现,写篇笔记记录一下
概述
React 渲染过程的分片其实是借助三个大的Loop完成的,我们可以比喻成三个齿轮,类似于汽车的变速箱。
- 第一个齿轮,js 的EventLoop,它是转动的最慢的那个。它通过宏任务/微任务来实现渲染过程中断,浏览器接管js线程,以达到处理高优先级的事情
- 第二个齿轮,Scheduler, 它是React里面任务的一个调度器,它会依据任务的优先级,执行任务队列中的任务。它执行任务的周期是5ms,这个时间切片里面,它会尽最大的努力执行任务队列里面的任务。如果超过5ms,它会结束当前的任务,开启一个宏任务,在宏任务中继续执行任务对立里面的任务。
- 第三个齿轮,React-reconciler里面的渲染过程,我们可以用workLoop来表示。它的渲染过程其实就是通过DFS遍历Fiber树,进行diff。创建一颗新的workInProgressTree。对每一个Fiber节点,就是一个原子操作。在对每颗Fiber进行操作前,它会主动查看是否超时,如果超时,它会自行中断,退出当前的执行。控制权交给第二个齿轮。
三个齿轮互相协调,达到了中断恢复的目的。第三个齿轮存在于第二个内部,第二个存在于第一个内部。第三个齿轮转速最快。
- 对于第二个齿轮,第三个齿轮,看了它内部的调度算法,下面通过简单的伪代码的方式,将它们内部的功能表现出来。我会尽量保留它内部的变量名,但是会忽略与算法无关的部分。
- 第二个齿轮,它的任务队列采用了最小堆结构来管理。这个里面有两个队列,taskQueue,timeQueue,taskQueue表示需要立刻执行的任务队列。而timeQueue表示的是延迟执行的任务队列。
// 这个是WorkLoop主函数,它的taskQueue是以最小堆的形式管理任务,源码中还有一个timeQueue,这边先跳过。
function workLoop(){
// 取得最紧急的任务
let currentTask=peek(taskQueue);
// 有两种情况退出这个循环,一个是任务执行完了,一个是超时了。
while(currentTask!=null){
// 这个是用来判断是否应该推出任务执行,因为超时了。需要让出主线程。
if(shouldYieldToHost()){
break;
}
const {callback}=currentTask;
// 执行某个具体的任务
if(typeof callback==='function'){
const nextTask=callback();
// 如果某个任务还没有执行完成,还有后续任务,这个标记子任务发现超时了,主动退出了。
if(nextTask!=null&&typeof nextTask ==='function'){
// 保存未完成的任务,以便于恢复执行
currentTask.callback=nextTask;
// 超时了,有未完成的任务,直接退出
return true;
}else{
// 这个标记子任务执行完成了,那么如果时间允许,努力执行队列里面下一个子任务
popup(taskQueue);
}
}else{
// 无效的任务,或者任务结束了。直接进入下一个循环
popup(taskQueue);
}
//从队列中取下一个任务
currentTask=peek(taskQueue);
}
// 这个标记是超时退出
if(currentTask!=null){
return true;
}else{
// 在下一个宏任务重新挂起这个workloop
schedulePerformWorkUntilDeadline();
// 标记当前没有未执行的任务了
return false;
}
}
// 这个是执行workloop的函数,如果workLoop还有为完成的任务,它将在下一个宏任务中继续
function performWorkUntilDeadline(){
const hasMoreTask=workLoop();
if(hasMoreTask){
schedulePerformWorkUntilDeadline();
}
}
// 这个函数是为了在宏任务中执行workloop的循环,它优先使用setImmediate,MessageChannel,最后使用setTimeout
function schedulePerformWorkUntilDeadline(){
if(typeof setImmediate==='function'){
setImmediate(performWorkUntilDeadline)
}else if(typeof MessageChannel !== 'undefined'){
const channel= new MessageChannel();
const port = channel.port2;
channel.port1.onmessage=performWorkUntilDeadline;
port.postMessage(null);
}else{
setTimeout(()=>{
performWorkUntilDeadline();
})
}
}
- 第三个齿轮,React-reconciler里面的
WorkLoop
.第三个齿轮其实是通过DFS算法来遍历Fiber树,完成render的过程。它的DFS算法不是采用递归的方式写的,而是采用了while
循环的方式,来避免递归带爆栈。下面的是我依据理解后复原的代码片段。关键的函数名称变量名称与源码是一致的。主要是为了突出齿轮转动的过程
interface Fiber{
child:Fiber,
parent:Fiber,
sibling:Fiber,
}
//这个会使用当前的Fiber树产生一颗新的Fiber树。
let workInProgress:Fiber;
// 这个是主要的WorkLoop
function workLoopConcurrent(root:Fiber){
workInProgress=root;
// 每一个Fiber节点就是一个独立的任务,每次循环都会检查是否超时
while(workInProgress!=null&&!shouldYeild()){
performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork:Fiber){
// 返回的是子节点,如果没有子节点就则当前节点工作完成,
const nextFiber = beginUnitOfWork(fiberNode);
if(nextFiber==null){
//当前节点子树的工作完成,要么返回它的下一个兄弟,如果没有兄弟就返回父级
completeUnitOfWork(fiberNode);
}else{
// 将它的子节点作为当前节点,DFS
workInProgress=nextFiber;;
}
}
function beginUnitOfWork(unitOfWork:Fiber){
// diff logic for current fiber
// 返回它的第一个孩子节点
if(unitOfWork.child!=null){
return unitOfWork.child;
}else{
return null;
}
}
// 返回下一个兄弟节点,或者父节点
function completeUnitOfWork(fiberNode:Fiber){
const {sibling,parent}=fiberNode;
if(sibling!=null){
workInProgress=sibling;
}else{
workInProgress=parent;
}
}
// 这个齿轮的入口在这个函数
function performConcurrentWorkOnRoot(
root: FiberRoot,
didTimeout: boolean,
)
{
// 源码中这个WorkLoop是被封装到内部函数中的,这边简化一下。实际上也是区分并发与步兵发的版本的。
let existStatus = workLoopConcurrent(root);
// 这个是源码里面的版本
// let existStatus = shouldTimeSlice
// ? renderRootConcurrent(root, lanes)
//: renderRootSync(root, lanes);
return existStatus=='completed'?null:performConcurrentWorkOnRoot.bind(null,root);
}
总结
- React V16/17默认是不会开启时间分片的,V18是配合Concurrent模式一起使用的。默认情况下我们是不会体会到时间分片的。例如,如果渲染时间比较长,页面该卡死还是卡死。渲染过程还是一撸到底。
- React 18里面添加了一个新的useTransaction,这个hook方法返回的
startTransaction
,这个里面的变更会使用上面Fiber的时间切片,它会放到一个低的优先级的任务里面去执行。这个是我们感知到时间切片的地方。 - 由于js语言的单线程的特性,所以,React框架层提出了多种优化的方式。实际上在Angular框架里面也提供了类似的功能。有机会再总结一下。