Fiber
1. 基础知识
1.1 屏幕刷新率
大部分浏览器刷新率是60桢/秒
页面是一帧一帧绘制出来,每秒达到60桢时,页面才会流畅,用户不会觉得卡顿
所以每桢的绘制时间是 1/60 = 16.66毫秒,1000ms/60桢 = 16ms,每帧执行时间是16毫秒,也就是说每帧的工作时间如果<=16ms时候,页面是流畅的,>16ms页面是卡顿的
1.2 桢
前面说到如果浏览器刷新率是60帧/秒,则每帧的绘制时间是16.66ms
每帧的开头包含样式计算,布局,绘制等
如果某个任务执行时间过长,浏览器则会卡顿,推迟渲染
1.3 requestAnimationFrame
由浏览器专门为动画提供的API
上一节一帧图可以看到requestAnimationFrame回调函数是在绘制之前,
下面通过一个简单的例子看下用法
function Request() {
const divRef = useRef<any>();
let start = 0;
function play() {
divRef.current.style.width = 0;
start = Date.now();
requestAnimationFrame(progress)
}
function progress() {
divRef.current.style.width = divRef.current.offsetWidth + 1 + 'px';
divRef.current.innerHTML = divRef.current.offsetWidth + '%';
if(divRef.current.offsetWidth < 100) {
const current = Date.now();
console.log(current - start); // 每次打印都是16ms左右
start = current;
requestAnimationFrame(progress)
}
}
return(
<div>
<div className="box" ref={divRef}></div>
<button onClick={() => play()}>开始</button>
</div>
)
}
1.4 requestIdleCallback
-
window.requestIdleCallback()
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。 -
正常帧任务完成后没超过
16 ms
,说明时间有富余,此时就会行requestIdleCallback
里注册的任务 -
浏览器先执行优先级高的任务,执行完如果在16.6ms之内还没用完,还有时间,requestIdleCallback就会向浏览器申请时间碎片执行自己的任务,执行完之后再把控制权交给浏览器 如果浏览器没有耗时任务,就会最多有50ms的时间来执行,这就是第三次剩余时间还有49ms的原因
-
目前只有Chrome支持
-
下面看一个具体的例子理解一下
<script>
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
const works = [
() => {
console.log("第1个任务开始");
sleep(20); //sleep(20);
console.log("第1个任务结束");
},
() => {
console.log("第2个任务开始");
sleep(20); //sleep(20);
console.log("第2个任务结束");
},
() => {
console.log("第3个任务开始");
sleep(20); //sleep(20);
console.log("第3个任务结束");
},
];
requestIdleCallback(workLoop, {
timeout: 1000 // 表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲
});
// callback:回调即空闲时需要执行的任务,该回调函数接收一个IdleDeadline对象作为入参。其中IdleDeadline对象包含:
// didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用
// timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少
function workLoop(deadline) {
console.log('本帧剩余时间', parseInt(deadline.timeRemaining()));
// didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用
// timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少
// console.log("本帧剩余时间", parseInt(deadline.timeRemaining()));
while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && works.length > 0) {
performUnitOfWork();
}
if (works.length > 0) {
console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
requestIdleCallback(workLoop);
}
}
function performUnitOfWork() {
works.shift()();
}
</script>
如果sleep(0),则剩余时间完全可以执行所有任务
1.5 MessageChannel
- MessageChannel API允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据
- MessageChannel创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,而一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据
- MessageChannel是一个宏任务
- React利用 MessageChannel模拟了requestIdleCallback,将回调延迟到绘制操作之后执行
下面看一个具体的例子:
<script>
const channel = new MessageChannel()
let pendingCallback;
let startTime;
let timeoutTime;
let perFrameTime = (1000 / 60);
let timeRemaining = () => perFrameTime - (Date.now() - startTime);
channel.port2.onmessage = () => {
if (pendingCallback) {
pendingCallback({ didTimeout: Date.now() > timeoutTime, timeRemaining });
}
}
window.requestIdleCallback = (callback, options) => {
timeoutTime = Date.now() + options.timeout;
requestAnimationFrame(() => {
startTime = Date.now();
pendingCallback = callback;
channel.port1.postMessage('hello');
})
}
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
const works = [
() => {
console.log("第1个任务开始");
sleep(30);//sleep(20);
console.log("第1个任务结束");
},
() => {
console.log("第2个任务开始");
sleep(30);//sleep(20);
console.log("第2个任务结束");
},
() => {
console.log("第3个任务开始");
sleep(30);//sleep(20);
console.log("第3个任务结束");
},
];
requestIdleCallback(workLoop, { timeout: 60 * 1000 });
function workLoop(deadline) {
console.log('本帧剩余时间', parseInt(deadline.timeRemaining()));
while ((deadline.timeRemaining() > 1 || deadline.didTimeout) && works.length > 0) {
performUnitOfWork();
}
if (works.length > 0) {
console.log(`只剩下${parseInt(deadline.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
requestIdleCallback(workLoop, { timeout: 60 * 1000 });
}
}
function performUnitOfWork() {
works.shift()();
}
</script>
2.Fiber
2.1 什么是Fiber
- React.createElement返回是一个虚拟dom,虚拟dom就是以js对象的方式描述dom的样子
- Fiber是一个执行单元,也是一种数据结构
- 我们可以通过某些调度策略合理分配CPU资源,提供用户相应程度
- 通过fiber架构,可以让render(Reconciliation协调)阶段变成可中断,适时的让出CPU执行权,让浏览器更快的和用户交互
- 每次渲染有两个阶段:Reconciliation(协调\render阶段)和Commit(提交阶段)
- 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,effect list;例如节点新增、删除、属性变更等等, 这些变更React 称之为副作用(Effect)
- 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断
- effect list 和完成顺序是一样的 // return 代表父节点
2.2 为什么要用fiber重构,原来的方式有什么问题?
1. 原来遍历虚拟dom是递归,深度优先遍历树结构,执行栈会越来越深,
2. 执行过程不能中断
2.3 Fiber的执行过程
- Render(Reconciliation 协调) 遍历规则
-
- 下一个节点: 先儿子,后弟弟,再叔叔 绿色(遍历顺序),蓝色(完成顺序)
-
- 自己所有字节点完成后自己完成
先儿子,A1, B1, C1,C1 没有儿子节点,所以C1完成,再C2,没有儿子,C2完成,B1子节点都完成,所以B1完成,再B2开始,B2没有儿子,没有叔叔,所以B2,完成,最后A1完成
2.4 complete 难点之一
A1text B1text C1text c1div c2text c2div b1div b2text b2div A1div divroot
在完成的时候要收集有副作用的fiber,然后组成effect list 每个fiber有两个属性 firstEffect指向第一个有副作用的子fiber lastEffect 指向最后一个有副作用子Fiber
2.5 commitWork
render结束后开始根据副作用nextEffect开始commit, 顺序就是完成的顺序
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
如果不是类组件,并且是PLACEMENT(插入),currentFiber.return.stateNode.appendChild(nextFiber.stateNode)