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框架里面也提供了类似的功能。有机会再总结一下。
posted @ 2024-08-16 14:56  kongshu  阅读(27)  评论(0编辑  收藏  举报