前言
- 本篇梳理下fiber架构的基础知识。
requestIdleCallback
-
这玩意只在浏览器中有,一般情况,浏览器在解析js时,如果任务太耗时,就会阻塞用户操作,界面成卡死状态。就像我前面无聊写的那个对账工具,计算量太大页面完全定住没法动。
-
而 requestIdleCallback,虽然不能解决js卡死问题,但如果将大的js任务拆解成一个个小的js任务,然后通过这个api进行分段调用,即可让用户觉得好像不卡一样。
-
这个api有2个参数,一个就是需要调用的函数,一个是超时设置,mdn介绍。
-
就是当浏览器空闲了,就会调用此函数,如果设置了deadline时间,那么不管浏览器是不是在忙,都得调用这个函数。当然浏览器在忙的时候是无法插入此函数的,只能走到空闲时间片时强制执行此函数。没看懂我这句的看mdn。
-
这个callback函数可以接收到一个deadline,里面有2个方法。deadlinemdn
-
一个是IdleDeadline.timeRemaining(),表示当前闲置周期的预估剩余毫秒数。
-
还有个IdleDeadline.didTimeout,表示是否超时,布尔值。
-
看个例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>yehuozhili</title>
</head>
<body>
<div id="root"></div>
</body>
<script>
function sleep(delay) {
for (let start = Date.now(); Date.now() - start <= delay;) { }
}
let works = [
() => {
console.log('11111')
sleep(20)
console.log('over1')
},
() => {
console.log('222')
// sleep(20)
console.log('over2')
},
() => {
console.log('3333')
// sleep(29)
console.log('over3')
}
]
function callback(deadline) {
console.log(deadline.timeRemaining(), deadline.didTimeout)
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0) {
performUnitOfWork()
}
//走到这表示不满足while条件,下一次时间不够了,进行递归调用
if (works.length > 0) {//当works为空就出去了
window.requestIdleCallback(callback, { timeout: 1000 })
}
}
function performUnitOfWork() {
works.shift()()
}
window.requestIdleCallback(callback, { timeout: 1000 })
</script>
</html>
- 可以发现最后输出:
index.html:75 14.42 false
index.html:59 11111
index.html:61 over1
index.html:75 9.28 false
index.html:64 222
index.html:66 over2
index.html:69 3333
index.html:71 over3
- 我们第一个任务的sleep(20)的时间超过了1帧所剩余的14.42ms,所以当执行完毕后,dealine.timeRemaining()会等于0,这样时间不够执行下一个任务,所以进入下面的递归idle调用判断。
- 这样递归idle,就可以把执行权交还给浏览器,使得浏览器看起来没死机的样子,然后当浏览器空闲时,进行递归完成下一个任务。
- 由于任务2和3我都把sleep注释掉了,所以任务2执行完,deadline.timeRemaining剩余时间大于0,所以够执行下一个任务,继续在while循环里运行下一个任务。因为在while里连续执行而不是递归调用,所以中间没有再次打印console.log那段。
- React的fiber就是基于这种思想,把原本diff的不可中断递归操作,拆解成多个小任务进行递归调用,使得diff的过程不会卡住浏览器。虽然计算量不变,但用户体验更好了。
MessageChannel
- 我们知道只有谷歌浏览器支持idle,react肯定不能搞得只能给谷歌浏览器用,所以兼容有使用messageChannel来制作相同功能。
- mdn地址
- 这玩意其实就是个宏任务,vue的nextTick以前是这玩意搞得,还看见有人用来搞深拷贝的骚操作。
- 一般浏览器都是会清空微任务,然后从宏任务队列里一个一个拿出来执行宏任务,如果有碰到微任务立马执行。而浏览器渲染是在宏任务执行之前。
- 所以,宏任务变相等于idle,不会阻塞渲染。
- 看个hack例子:
function sleep(delay) {
for (let start = Date.now(); Date.now() - start <= delay;) { }
}
let root = document.getElementById('root')
let works = [
() => {
console.log('11111')
root.textContent = '111'
sleep(20)
console.log('over1')
},
() => {
console.log('222')
// sleep(20)
root.textContent = '2222'
console.log('over2')
},
() => {
console.log('3333')
root.textContent = '3333'
sleep(29)
console.log('over3')
},
() => {
console.log('444')
root.textContent = '4444'
console.log('over4')
}
]
function callback(deadline) {
console.log(deadline.timeRemaining(), deadline.didTimeout)
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && works.length > 0) {
performUnitOfWork()
}
//走到这表示不满足while条件,下一次时间不够了,进行递归调用
if (works.length > 0) {//当works为空就出去了
// window.requestIdleCallback(callback, { timeout: 1000 })
window.myIdle(callback, { timeout: 1000 })
}
}
function performUnitOfWork() {
works.shift()()
}
//window.requestIdleCallback(callback, { timeout: 1000 })
let channel = new MessageChannel()
let perFrameTime = (1000 / 60);
let timeRemaining = () => perFrameTime - (Date.now() - startTime);//1帧减去当前减开始
let pendingCallback;
let timeoutTime, startTime
channel.port2.onmessage = () => {//接收消息
if (pendingCallback) {
pendingCallback({ didTimeout: Date.now() > timeoutTime, timeRemaining });
}
}
window.myIdle = (callback, options) => {
timeoutTime = Date.now() + options.timeout;//现在时间戳+传入
startTime = Date.now();
pendingCallback = callback;//赋值callback
channel.port1.postMessage('');
// startTime = Date.now();
// setTimeout(() => {
// callback({ didTimeout: Date.now() > timeoutTime, timeRemaining });
// });
}
window.myIdle(callback, { timeout: 1000 })
- 还是刚才那些代码,只是写了个myidle代替requestIdleCallback。
- 这里不用channel用setTimeout一样效果。
- 这里速度有点快,可以发现数字会有个闪动过程,这就代表没有阻塞渲染。
- 而阻塞渲染会怎样呢?将while循环内改为true,则不会进行递归调用,一直在主栈中进行调用,浏览器中看到的数字不会闪动,而是一出来已经确定的数字。