模仿实现react fiber 任务调度

在你已经知道什么是fiber以及react为什么需要fiber,并且知道raf和ric这两个api的前提下阅读

react fiber也叫协程或者纤维,是把react任务分成一个个小任务,再通过调度在浏览器空闲时间来执行相关任务,避免阻塞浏览器渲染、响应用户行为等高优先级的任务。
本文只是模仿react实现任务在浏览器的空闲时间的中断、恢复和执行,并不代表是react的全部源码

首先用数据模拟一个fiber树(双向链表)

    // 模拟耗时任务
    function sleep(delay) {
        for (let start = Date.now(); Date.now() - start <= delay;) {
        }
    }
    // 模拟fiber节点
    let A1 = {type: 'div', key: 'A1'}
    let B1 = {type: 'div', key: 'B1', return: A1}
    let B2 = {type: 'div', key: 'B2', return: A1}
    let C1 = {type: 'div', key: 'C1', return: B1}
    let C2 = {type: 'div', key: 'C2', return: B1}
    A1.child = B1
    B1.sibling = B2
    B1.child = C1
    C1.sibling = C2
    let rootFiber = A1

使用 requestIdleCallback 来实现调度

 let nextUnitOfWork=null //下一个执行单元
    function workLoop(deadline){
        console.log(`本次调度开始,本帧剩余时间${deadline.timeRemaining()}`)
       while ((deadline.timeRemaining()>0||deadline.didTimeout)&&nextUnitOfWork){
           nextUnitOfWork=performUnitOfWork(nextUnitOfWork)
       }
        console.log('结束控制交给浏览器')
      // 没有下次执行单元即任务结束
       if(!nextUnitOfWork){
           console.log('render阶段结束了')
       }else{
           window.requestIdleCallback(workLoop,{timeout:1000})
       }
    }
    function performUnitOfWork(fiber){
        beginWork(fiber) // 处理此fiber
        if(fiber.child){ // 如果有儿子,返回大儿子
            return fiber.child
        }// 如果没有儿子,说明此fiber已经完成了
        while(fiber){
            completeUnitOfWork(fiber)
            if(fiber.sibling){
                return fiber.sibling  // 如果有弟弟就返回弟弟
            }
            fiber=fiber.return // 此时while循中的fiber为上一次的父亲
        }
    }
    function completeUnitOfWork(fiber){
        // 收集effect
        console.log(fiber.key,'结束')
    }
    function beginWork(fiber){
       // 调和阶段
        console.log(fiber.key,'开始')
    }
    nextUnitOfWork=rootFiber
    // debugger
    // workLoop(rootFiber)
    window.requestIdleCallback(workLoop,{timeout:1000})

总结:
1、设置 requestIdleCallback 回调,告诉浏览器在空闲执行 workLoop
2、在 workLoop 中,nextUnitOfWork 为下一个执行单元,便于中断后恢复执行,requestIdleCallback的回调函数有一个默认参数deadline(包含timeRemaining方法:检查本帧是否还有空闲时间,didTimeout:是否已经超过过期时间)。判断是否还有剩余时间和还有剩余任务,有就执行任务,没有则交还控制权给浏览器。如果还有任务未执行,则告诉浏览在下一帧空闲时执行任务。

使用 requestAnimationFrame 和MessageChannel 实现

    // 帧截止时间
    let frameDeadline = 0
    // 初始当前帧率为30fps,则帧执行时间为33ms
    // 上一帧执行时间
    let previousFrameTime = 33
    // 30fps下,每一帧执行时间
    let activeFrameTime = 33
    // 执行回调
    let callback = null

    // 上一个工作单元,默认为根节点
    let nextUnitOfWork = rootFiber

    // 计算rIC参数,剩余时间 帧截止时间-js执行时间 即为剩余时间
    let frameDeadlineObject = {
        timeRemaining:
            typeof performance === "object" && typeof performance.now === "function"
                ? function () {
                    return frameDeadline - performance.now()
                }
                : function () {
                    return frameDeadline - Date.now()
                },
    }

    // 计算调整帧率,得出帧截止时间
    function animationTick(rafTime) {
        // 计算出下一帧执行时间,这里的frameDeadline为上一帧的截止时间
        let nextFrameTime = rafTime - frameDeadline + activeFrameTime
        // 如果连续2帧的执行时间都小于帧执行时间,则说明可以提高帧率
        if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
            if (nextFrameTime < 8) {
                // 最高提高的120fps,
                nextFrameTime = 8
            }
            // 取连续2帧中执行时间较大的,防止执行超过帧截止时间
            activeFrameTime =
                nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime
        } else {
            previousFrameTime = nextFrameTime
        }
        // 计算出帧截止时间,大概结束时间 = 默认这是一帧的开始时间 + 一帧大概耗时
        frameDeadline = rafTime + activeFrameTime
        console.log('本帧结束时间',frameDeadline)
    }

    // 发布订阅
    let channel = new MessageChannel(); // 该对象实例有且只有两个端口,并且可以相互收发事件。
    let port1 = channel.port1;
    let port2 = channel.port2;
    // 订阅消息
    port2.onmessage = () => {
        // 执行任务
        callback(frameDeadlineObject)
    }
    // 模拟实现requestIdleCallback
    window.requestIdleCallbackPolyfill = function (cb) {
        requestAnimationFrame(rafStartTime => {
            animationTick(rafStartTime)
            callback = cb
            // 发布消息
            port1.postMessage(null);
        });
    }

    // 任务队列
    function workLoop(deadline) {
        console.log(`本次调度开始,本帧剩余时间${deadline.timeRemaining()}`)
        while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) {
            // console.log(`本帧的剩余时间${deadline.timeRemaining()}`)
            nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
        }
        console.log('结束控制交给浏览器')
        // 没有下次执行单元即任务结束
        if (!nextUnitOfWork) {
            console.log('render阶段结束了')
        } else {
            window.requestIdleCallbackPolyfill(workLoop, {timeout: 1000})
        }
    }

    // 执行任务单元
    function performUnitOfWork(fiber) {
        beginWork(fiber) // 处理此fiber
        if (fiber.child) { // 如果有儿子,返回大儿子
            return fiber.child
        }// 如果没有儿子,说明此fiber已经完成了
        while (fiber) {
            completeUnitOfWork(fiber)
            if (fiber.sibling) {
                return fiber.sibling  // 如果有弟弟就返回弟弟
            }
            fiber = fiber.return // 此时while循中的fiber为上一次的父亲
        }
    }

    // 工作完成单元
    function completeUnitOfWork(fiber) {
        console.log(fiber.key, '结束')
    }

    // 开始任务
    function beginWork(fiber) {
        sleep(10)
        console.log(fiber.key, '开始')
    }

    window.requestIdleCallbackPolyfill(workLoop, {timeout: 1000})

requestAnimationFrame(callback) 会在浏览器每次重绘前执行 callback 回调, 每次 callback 执行的时机都是浏览器刷新下一帧渲染周期的起点上。
requestAnimationFrame(callback) 的回调 callback 回调参数 timestamp 是回调被调用的时间,也就是当前帧的起始时间

总结:1、大致流程和使用requestIdleCallback一样,关键点在于如何得到当前帧的剩余时间(剩余时间=结束时间-已耗费的时间)
2、requestIdleCallbackPolyfill 借助requestAnimationFrame在animationTick函数中需要做的就是调整帧率和计算出帧的截止结束时间,activeFrameTime是一个假定时间(在react中的初始化假定时间也是33ms),保留需要执行的回调函数(若在rAF函数中执行,会加长帧的执行时间,因此将回调函数放在onmessage中去异步执行)
3、performance.now()这个web api表示为从time origin之后到当前调用时经过的时间 参考地址
4,最后在workLoop中,判断是否还有剩余时间执行任务

你可能感兴趣的:(模仿实现react fiber 任务调度)