在你已经知道什么是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中,判断是否还有剩余时间执行任务