模仿實(shí)現(xiàn)react fiber 任務(wù)調(diào)度

在你已經(jīng)知道什么是fiber以及react為什么需要fiber忌愚,并且知道raf和ric這兩個(gè)api的前提下閱讀

react fiber也叫協(xié)程或者纖維,是把react任務(wù)分成一個(gè)個(gè)小任務(wù)寸宵,再通過(guò)調(diào)度在瀏覽器空閑時(shí)間來(lái)執(zhí)行相關(guān)任務(wù),避免阻塞瀏覽器渲染、響應(yīng)用戶行為等高優(yōu)先級(jí)的任務(wù)刊苍。
本文只是模仿react實(shí)現(xiàn)任務(wù)在瀏覽器的空閑時(shí)間的中斷、恢復(fù)和執(zhí)行濒析,并不代表是react的全部源碼

首先用數(shù)據(jù)模擬一個(gè)fiber樹(shù)(雙向鏈表)

    // 模擬耗時(shí)任務(wù)
    function sleep(delay) {
        for (let start = Date.now(); Date.now() - start <= delay;) {
        }
    }
    // 模擬fiber節(jié)點(diǎn)
    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 來(lái)實(shí)現(xiàn)調(diào)度

 let nextUnitOfWork=null //下一個(gè)執(zhí)行單元
    function workLoop(deadline){
        console.log(`本次調(diào)度開(kāi)始正什,本幀剩余時(shí)間${deadline.timeRemaining()}`)
       while ((deadline.timeRemaining()>0||deadline.didTimeout)&&nextUnitOfWork){
           nextUnitOfWork=performUnitOfWork(nextUnitOfWork)
       }
        console.log('結(jié)束控制交給瀏覽器')
      // 沒(méi)有下次執(zhí)行單元即任務(wù)結(jié)束
       if(!nextUnitOfWork){
           console.log('render階段結(jié)束了')
       }else{
           window.requestIdleCallback(workLoop,{timeout:1000})
       }
    }
    function performUnitOfWork(fiber){
        beginWork(fiber) // 處理此fiber
        if(fiber.child){ // 如果有兒子,返回大兒子
            return fiber.child
        }// 如果沒(méi)有兒子,說(shuō)明此fiber已經(jīng)完成了
        while(fiber){
            completeUnitOfWork(fiber)
            if(fiber.sibling){
                return fiber.sibling  // 如果有弟弟就返回弟弟
            }
            fiber=fiber.return // 此時(shí)while循中的fiber為上一次的父親
        }
    }
    function completeUnitOfWork(fiber){
        // 收集effect
        console.log(fiber.key,'結(jié)束')
    }
    function beginWork(fiber){
       // 調(diào)和階段
        console.log(fiber.key,'開(kāi)始')
    }
    nextUnitOfWork=rootFiber
    // debugger
    // workLoop(rootFiber)
    window.requestIdleCallback(workLoop,{timeout:1000})

總結(jié):
1悼枢、設(shè)置 requestIdleCallback 回調(diào)埠忘,告訴瀏覽器在空閑執(zhí)行 workLoop
2、在 workLoop 中馒索,nextUnitOfWork 為下一個(gè)執(zhí)行單元莹妒,便于中斷后恢復(fù)執(zhí)行,requestIdleCallback的回調(diào)函數(shù)有一個(gè)默認(rèn)參數(shù)deadline(包含timeRemaining方法:檢查本幀是否還有空閑時(shí)間绰上,didTimeout:是否已經(jīng)超過(guò)過(guò)期時(shí)間)旨怠。判斷是否還有剩余時(shí)間和還有剩余任務(wù),有就執(zhí)行任務(wù)蜈块,沒(méi)有則交還控制權(quán)給瀏覽器鉴腻。如果還有任務(wù)未執(zhí)行迷扇,則告訴瀏覽在下一幀空閑時(shí)執(zhí)行任務(wù)。

使用 requestAnimationFrame 和MessageChannel 實(shí)現(xiàn)

    // 幀截止時(shí)間
    let frameDeadline = 0
    // 初始當(dāng)前幀率為30fps爽哎,則幀執(zhí)行時(shí)間為33ms
    // 上一幀執(zhí)行時(shí)間
    let previousFrameTime = 33
    // 30fps下蜓席,每一幀執(zhí)行時(shí)間
    let activeFrameTime = 33
    // 執(zhí)行回調(diào)
    let callback = null

    // 上一個(gè)工作單元,默認(rèn)為根節(jié)點(diǎn)
    let nextUnitOfWork = rootFiber

    // 計(jì)算rIC參數(shù)课锌,剩余時(shí)間 幀截止時(shí)間-js執(zhí)行時(shí)間 即為剩余時(shí)間
    let frameDeadlineObject = {
        timeRemaining:
            typeof performance === "object" && typeof performance.now === "function"
                ? function () {
                    return frameDeadline - performance.now()
                }
                : function () {
                    return frameDeadline - Date.now()
                },
    }

    // 計(jì)算調(diào)整幀率厨内,得出幀截止時(shí)間
    function animationTick(rafTime) {
        // 計(jì)算出下一幀執(zhí)行時(shí)間,這里的frameDeadline為上一幀的截止時(shí)間
        let nextFrameTime = rafTime - frameDeadline + activeFrameTime
        // 如果連續(xù)2幀的執(zhí)行時(shí)間都小于幀執(zhí)行時(shí)間渺贤,則說(shuō)明可以提高幀率
        if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
            if (nextFrameTime < 8) {
                // 最高提高的120fps雏胃,
                nextFrameTime = 8
            }
            // 取連續(xù)2幀中執(zhí)行時(shí)間較大的,防止執(zhí)行超過(guò)幀截止時(shí)間
            activeFrameTime =
                nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime
        } else {
            previousFrameTime = nextFrameTime
        }
        // 計(jì)算出幀截止時(shí)間志鞍,大概結(jié)束時(shí)間 = 默認(rèn)這是一幀的開(kāi)始時(shí)間 + 一幀大概耗時(shí)
        frameDeadline = rafTime + activeFrameTime
        console.log('本幀結(jié)束時(shí)間',frameDeadline)
    }

    // 發(fā)布訂閱
    let channel = new MessageChannel(); // 該對(duì)象實(shí)例有且只有兩個(gè)端口瞭亮,并且可以相互收發(fā)事件。
    let port1 = channel.port1;
    let port2 = channel.port2;
    // 訂閱消息
    port2.onmessage = () => {
        // 執(zhí)行任務(wù)
        callback(frameDeadlineObject)
    }
    // 模擬實(shí)現(xiàn)requestIdleCallback
    window.requestIdleCallbackPolyfill = function (cb) {
        requestAnimationFrame(rafStartTime => {
            animationTick(rafStartTime)
            callback = cb
            // 發(fā)布消息
            port1.postMessage(null);
        });
    }

    // 任務(wù)隊(duì)列
    function workLoop(deadline) {
        console.log(`本次調(diào)度開(kāi)始固棚,本幀剩余時(shí)間${deadline.timeRemaining()}`)
        while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextUnitOfWork) {
            // console.log(`本幀的剩余時(shí)間${deadline.timeRemaining()}`)
            nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
        }
        console.log('結(jié)束控制交給瀏覽器')
        // 沒(méi)有下次執(zhí)行單元即任務(wù)結(jié)束
        if (!nextUnitOfWork) {
            console.log('render階段結(jié)束了')
        } else {
            window.requestIdleCallbackPolyfill(workLoop, {timeout: 1000})
        }
    }

    // 執(zhí)行任務(wù)單元
    function performUnitOfWork(fiber) {
        beginWork(fiber) // 處理此fiber
        if (fiber.child) { // 如果有兒子,返回大兒子
            return fiber.child
        }// 如果沒(méi)有兒子统翩,說(shuō)明此fiber已經(jīng)完成了
        while (fiber) {
            completeUnitOfWork(fiber)
            if (fiber.sibling) {
                return fiber.sibling  // 如果有弟弟就返回弟弟
            }
            fiber = fiber.return // 此時(shí)while循中的fiber為上一次的父親
        }
    }

    // 工作完成單元
    function completeUnitOfWork(fiber) {
        console.log(fiber.key, '結(jié)束')
    }

    // 開(kāi)始任務(wù)
    function beginWork(fiber) {
        sleep(10)
        console.log(fiber.key, '開(kāi)始')
    }

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

requestAnimationFrame(callback) 會(huì)在瀏覽器每次重繪前執(zhí)行 callback 回調(diào), 每次 callback 執(zhí)行的時(shí)機(jī)都是瀏覽器刷新下一幀渲染周期的起點(diǎn)上。
requestAnimationFrame(callback) 的回調(diào) callback 回調(diào)參數(shù) timestamp 是回調(diào)被調(diào)用的時(shí)間此洲,也就是當(dāng)前幀的起始時(shí)間

總結(jié):1唆缴、大致流程和使用requestIdleCallback一樣,關(guān)鍵點(diǎn)在于如何得到當(dāng)前幀的剩余時(shí)間(剩余時(shí)間=結(jié)束時(shí)間-已耗費(fèi)的時(shí)間)
2黍翎、requestIdleCallbackPolyfill 借助requestAnimationFrame在animationTick函數(shù)中需要做的就是調(diào)整幀率和計(jì)算出幀的截止結(jié)束時(shí)間面徽,activeFrameTime是一個(gè)假定時(shí)間(在react中的初始化假定時(shí)間也是33ms),保留需要執(zhí)行的回調(diào)函數(shù)(若在rAF函數(shù)中執(zhí)行匣掸,會(huì)加長(zhǎng)幀的執(zhí)行時(shí)間趟紊,因此將回調(diào)函數(shù)放在onmessage中去異步執(zhí)行)
3、performance.now()這個(gè)web api表示為從time origin之后到當(dāng)前調(diào)用時(shí)經(jīng)過(guò)的時(shí)間 參考地址
4碰酝,最后在workLoop中霎匈,判斷是否還有剩余時(shí)間執(zhí)行任務(wù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市送爸,隨后出現(xiàn)的幾起案子铛嘱,更是在濱河造成了極大的恐慌,老刑警劉巖袭厂,帶你破解...
    沈念sama閱讀 221,888評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件墨吓,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡纹磺,警方通過(guò)查閱死者的電腦和手機(jī)帖烘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)橄杨,“玉大人秘症,你說(shuō)我怎么就攤上這事照卦。” “怎么了乡摹?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,386評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵役耕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我聪廉,道長(zhǎng)蹄葱,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,726評(píng)論 1 297
  • 正文 為了忘掉前任锄列,我火速辦了婚禮,結(jié)果婚禮上惯悠,老公的妹妹穿的比我還像新娘邻邮。我一直安慰自己,他們只是感情好克婶,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,729評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布筒严。 她就那樣靜靜地躺著,像睡著了一般情萤。 火紅的嫁衣襯著肌膚如雪鸭蛙。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,337評(píng)論 1 310
  • 那天筋岛,我揣著相機(jī)與錄音娶视,去河邊找鬼。 笑死睁宰,一個(gè)胖子當(dāng)著我的面吹牛肪获,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播柒傻,決...
    沈念sama閱讀 40,902評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼孝赫,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了红符?” 一聲冷哼從身側(cè)響起青柄,我...
    開(kāi)封第一講書(shū)人閱讀 39,807評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎预侯,沒(méi)想到半個(gè)月后致开,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,349評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡萎馅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,439評(píng)論 3 340
  • 正文 我和宋清朗相戀三年喇喉,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片校坑。...
    茶點(diǎn)故事閱讀 40,567評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡拣技,死狀恐怖千诬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情膏斤,我是刑警寧澤徐绑,帶...
    沈念sama閱讀 36,242評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站莫辨,受9級(jí)特大地震影響傲茄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜沮榜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,933評(píng)論 3 334
  • 文/蒙蒙 一盘榨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蟆融,春花似錦草巡、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,420評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至弥喉,卻和暖如春郁竟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背由境。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,531評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工棚亩, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人虏杰。 一個(gè)月前我還...
    沈念sama閱讀 48,995評(píng)論 3 377
  • 正文 我出身青樓蔑舞,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親嘹屯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子攻询,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,585評(píng)論 2 359