React18原理: 再聊Fiber架构下的时间分片
时间分片
- react的任务可以被打断,其实就是基于时间分片的
- 人眼最高能识别的帧数不超过30帧,电影的帧数差不多是在24
- 浏览器的帧率一般来说是60帧,也就是每秒60个画面, 平均一个画面大概是16.5毫秒左右
- 浏览器正常的工作流程是运算渲染,运算,渲染运算渲染
- 在浏览器里面一个运算,加上一个渲染就是一帧
- 总的来讲,可以理解为下面这张图
- 比如 frame 是一帧,一个 Frame 就是16毫秒左右
- 黑色部分是浏览器的渲染,蓝色部分是js的运算
- 在16毫秒以内(一帧), 浏览器会重新渲染画面,然后再加上JS的一轮事件循环的执行
- 根据任务队列循环下去,一秒 60 帧,每一帧都是 js的执行 + 浏览器的渲染
- 但是, js它是单线程的, 会阻塞浏览器渲染, 假如 js执行时间超长,占了 3 ~ 4帧
- js执行的时候,浏览器是不能渲染的,那这个时候会有页面卡顿的感觉
- 实际上这个时候是 js 在执行
- 这个也是react它去递归渲染的时候的问题
- 递归渲染,它就是属于长进程,相当于在 render 的时候 js 一直把渲染进程给卡住
- 这个是哥很苦恼的问题,所以诞生了fiber架构, react希望能够把任务分片处理
- 这个时候就提到了一个概念,就是 fiber reconciler 要做的事情
- 它如何让我们把时间分片,然后让又让浏览器不卡顿的呢?
- 其实特别的巧妙,谷歌浏览器底层提供的一个东西叫做 requestIdleCallback
- 前面说到一帧(16ms左右) 是 渲染 + js的执行
- 有时候浏览器比较空闲,有可能一帧不需要 16ms,可能需要6ms, 那剩下的10ms可以执行长任务
- 当剩下的10ms用完,可以把浏览器的渲染权利再还给浏览器
- 这个时候进入下一帧的浏览器的画面,继续渲染,渲染完之后又有剩余时间
- 接着再执行这个长进程,简单来说,就是把长进程拆分成一个个很小的任务
- 它利用浏览器每一帧的空闲时间去执行,这样就实现了任务的打断,而且还不阻塞浏览器的渲染
- 也就是说,本来一个任务要执行1秒,但是实际上react的fiber架构可能让这个1秒执行的时间更长
- 因为任务的拆分其实是增加了这个计算的开销的,但是,它却是在我们每一帧的空闲时间去执行的
- 虽然执行的整体时间可能变长,但是让用户的感觉没有那么卡顿,所以它的体验是提升了的
- 参考之前 React 16的时间片:https://blog.csdn.net/Tyro_java/article/details/135586572
关于 requestIdleCallback
-
文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
-
window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用
-
这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
-
函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序
-
requestIdleCallback(callback)
-
requestIdleCallback(callback, options)
- callback
- 一个在事件循环空闲时即将被调用的函数的引用。函数会接收到一个名为 IdleDeadline 的参数
- 这个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态
- options 可选
- 包括可选的配置参数。具有如下属性
- timeout
- 如果指定了 timeout,并且有一个正值,而回调在 timeout 毫秒过后还没有被调用
- 那么回调任务将放入事件循环中排队,即使这样做有可能对性能产生负面影响
- timeout
- 包括可选的配置参数。具有如下属性
- callback
-
返回值是一个ID,可以把它传入
Window.cancelIdleCallback()
方法来结束回调
requestIdleCallback 和 requestAnimationFrame 的区别
1 )react fiber 引起的关注
- 组件树转换为链表,可分段渲染
- 渲染时可以暂停,去执行其他高优先任务,空闲时再继续渲染
- 如何判断空闲?requestIdleCallback
2 ) 区别
-
requestAnimationFrame 每次渲染完都会执行,高优
-
requestIdleCallback 空闲时才会执行,低优
let curWidth = 100 const maxWidth = 400 function addWidth() { curWidth = curWidth + 3 box.style.width = `${curWidth} px` if (curWidth < maxWidth) { widndow.requestAnimationFrame(addWidth) // 时间不用自己控制 高优先级 widndow.requestIdleCallback(addWidth) // 时间不用自己控制 繁忙时不会执行 } } addWidth()
-
对比
console.info('start') window.requestIdleCallback(()=>{ console.log('requestIdleCallback') }) window.requestAnimationFrame(()=>{ console.log('requestAnimationFrame') }) setTimeout(()=>{ console.log('setTimeout') }) console.info('end')
-
执行顺序
- start
- end
- timeout 优先级更高
- requestAnimationFrame 宏任务优先级较高
- requestIdleCallback 宏任务优先级较低
-
总结
- 两者都是宏任务
- 需要等待dom渲染完才会执行