在fiber出现之前,react的架构体系只有协调器reconciler和渲染器render。
当前有新的update时,react会递归所有的vdom节点,如果dom节点过多,会导致其他事件影响滞后,造成卡顿。即之前的react版本无法中断工作过程,一旦递归开始无法停留下来。
为了解决这一系列问题,react历时多年重构了底层架构,引入了fiber。fiber的出现使得react能够异步可中断工作任务,并且可以在浏览器空闲时,从中断处继续往下工作。
屏幕刷新率
目前大多数设备的屏幕刷新率为 60 次/秒
浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致
页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿
每个帧的预算时间是16.66 毫秒 (1秒/60)
1s 60帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms。所以我们书写代码时力求不让一帧的工作量超过 16ms
每个帧的开头包括样式计算、布局和绘制
JavaScript执行 Javascript引擎和页面渲染引擎在同一个渲染线程,GUI渲染和Javascript执行两者是互斥的
如果某个任务执行时间过长,浏览器会推迟渲染
requestIdleCallback
我们希望快速响应用户,让用户觉得够快,不能阻塞用户的交互
requestIdleCallback使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
正常帧任务完成后没超过16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务
<body>
<script>
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
const works = [
() => {
console.log("第1个任务开始");
sleep(0);//sleep(20);
console.log("第1个任务结束");
},
() => {
console.log("第2个任务开始");
sleep(0);//sleep(20);
console.log("第2个任务结束");
},
() => {
console.log("第3个任务开始");
sleep(0);//sleep(20);
console.log("第3个任务结束");
},
];
requestIdleCallback(workLoop, { timeout: 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);
}
}
function performUnitOfWork() {
works.shift()();
}
</script>
</body>
MessageChannel
目前 requestIdleCallback 目前只有Chrome支持
所以目前 React利用 MessageChannel模拟了requestIdleCallback,将回调延迟到绘制操作之后执行
MessageChannel API允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据
MessageChannel创建了一个通信的管道,这个管道有两个端口,每个端口都可以通过postMessage发送数据,而一个端口只要绑定了onmessage回调方法,就可以接收从另一个端口传过来的数据
MessageChannel是一个宏任务
var channel = new MessageChannel();
var port1 = channel.port1;
var port2 = channel.port2;
port1.onmessage = function(event) {
console.log("port1收到来自port2的数据:" + event.data);
}
port2.onmessage = function(event) {
console.log("port2收到来自port1的数据:" + event.data);
}
port1.postMessage("发送给port2");
port2.postMessage("发送给port1");
演示代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<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');
})
/* startTime = Date.now();
setTimeout(() => {
callback({ didTimeout: Date.now() > timeoutTime, timeRemaining });
}); */
}
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>
</body>
</html>