模仿实现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中,判断是否还有剩余时间执行任务

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351