在你已經(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ù)