React系列:第九回(concurrentmode、fiber)
本文介绍,react快速渲染的原理
如何避免卡死?
在第一回中,我们通过手撕myCreateElement和myRender实现了基本的功能,但是仔细观察下之前写的render,是否存在什么问题?
const myRender = (element, container) => {
const dom = element.type === 'text'? document.createTextNode(element.props.nodeValue): document.createElement(element.type)
Object.keys(element.props).filter((item) => item !== 'children').forEach((item) => dom[item] = element.props[item])
element?.props?.children?.forEach((child) => myRender(child, dom))
container.appendChild(dom)
}
render做的事情很清晰,就是根据element的信息,生成真实的dom然后挂载,对于其中的子节点,我们只是粗暴的递归之。设想一下,如果我们传入的是一个巨深的虚拟dom,那么会发生什么?render这个函数的耗时势必也会剧增,一旦开始执行,就会执行到底。即:从首次执行render开始到最终结束,期间的浏览器都是处于阻塞的状态。此时是无法响应任何用户的操作的。俗称卡死。
那么react底层是如何做优化的呢?答案也很清晰,利用一个类似apirequestIdleCallback的效果,异步执行.
requestIdleCallback
该函数的作用,就是能够观察浏览器在处理完每帧的工作之后,是否存在空余时间。如果有,就执行requestIdleCallback的回调,没有,则忽略。
因此,react能够做到快速的底层思路就有了:把render的操作变成一个个的任务单元。这些任务单元执行的前提条件是:当前帧存在空余时间,有则执行,没有下一帧继续判断执行。
既然提到了requestIdleCallback,那就不得不再提一下另一个api:requestAnimationFrame。这个api我们之前讲屏幕刷新率的时候也提到过。那么两者有什么区别呢?
两者都会在每一帧执行注册任务,本质区别在于优先级:raf注册的任务属于高优先级,尽力保证每一帧都会执行一次。而requestIdleCallback注册的任务则属于低优先级,只有当前帧存在剩余时间才会执行,有可能永远不执行。
测试优先级:
const workLoop2 = () => {
console.log('requestIdleCallback')
requestIdleCallback(workLoop2)
}
requestIdleCallback(workLoop2)
const workLoop1 = () => {
console.log('requestAnimationFrame')
requestAnimationFrame(workLoop1)
}
requestAnimationFrame(workLoop1)
效果如下:
从实测效果来看,即使raf代码后执行,注册的任务优先级仍然高于requestIdleCallback,甚至还出现了raf执行了四次之后才执行了一次requestIdleCallback.
顺便提一下的这俩的一个细节:dealine参数
const workLoop2 = (deadline) => {
console.log('requestIdleCallback')
console.log('requestIdleCallback-deadline>>2', deadline)
console.log('requestIdleCallback-deadline>>2-----', deadline.timeRemaining())
requestIdleCallback(workLoop2)
}
requestIdleCallback(workLoop2)
const workLoop1 = (deadline) => {
console.log('requestAnimationFrame')
console.log('requestAnimationFrame-deadline>>1', deadline)
requestAnimationFrame(workLoop1)
}
requestAnimationFrame(workLoop1)
打印如下:
dealine用来提供额外的时间信息, 其中requestIdleCallback的deadline存在一个timeRemaining方法获取当前帧剩余时间
react的底层并未通过timeRemaining获取剩余时间,而是自创了一套schedule
借助requestIdleCallback改造实现
流程:
总体思路: 为了避免渲染时dom过深,导致耗时过长甚至卡死, 借助requestIdleCallback,将之前render做的事情,分开执行。当前浏览器是否空闲(即有无剩余时间),有,则判断当前是否存在下一个任务单元,有则执行。执行过程中,无时间了,打断,有则继续执行。直至最后完毕。
Fiber架构。其核心:可中断、可恢复、优先级
Fiber架构对生命周期的影响
fiber也是一种数据结构,类似vnode
在vue中,vnode --> 真实dom
在react中, vnode(ReactElement) --> fiber ---> 真实dom
大概长下面这样:
代码
let nextUniteWork = null
const myCreateElement = (type, props, ...children) => {
return {
type: type,
props: {
...props,
children: children.map((child) => typeof child === 'object'? child: createTextNode(child))
},
}
}
const createTextNode = (child) => {
return {
type: 'text',
props: {
nodeValue: child,
children: []
}
}
}
const createDom = (fiber) => {
const dom = fiber.type === 'text'? document.createTextNode(fiber.props.nodeValue): document.createElement(fiber.type)
return dom
}
const performUniteOfWork = (fiber) => {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
console.log('fiber>>>',fiber)
console.log('fiber.dom>>>',fiber.dom)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
const elements = fiber?.props?.children
console.log('elements>>', elements)
let preSibling = null
elements?.forEach((childElement, index) => {
const newFiber = {
parent: fiber,
props: childElement.props,
type: childElement.type,
dom: null
}
if (index === 0) {
fiber.child = newFiber
} else {
preSibling.sibling = newFiber
}
preSibling = newFiber
});
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while(nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
const workLoop = (deadline) => {
let shouldYield = true
console.warn('执行>>>loop')
while (nextUniteWork && shouldYield) {
console.log('执行>>>任务')
nextUniteWork = performUniteOfWork(nextUniteWork)
shouldYield = deadline.timeRemaining() > 100
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
const myRender = (element, container) => {
console.log('element>>', element)
nextUniteWork = {
dom: container,
props: {
children: [element]
}
}
}
效果如下
总体的逻辑就是:一个节点一个节点的往深处走,创建dom,添加父亲兄弟节点信息,走到尽头,在一步步的往回收缩的走,直到扫完所有节点,最终回到跟节点
注意:react的底层实现中,因为存在兼容性的问题,并没有用requestIdleCallback,而是用的自己实现了的一套工具。
补充题:fiber的本质解决了什么问题?同样的问题,vue中是如何解决的?
我发现上海那边很喜欢问这问题。fiber本质解决了长任务阻塞主线程的问题。在 Fiber 出现前,React 的协调(Reconciliation)过程是同步且不可中断的。当组件树庞大时,递归比对虚拟 DOM 会占用主线程较长时间,导致页面卡顿(无法及时响应用户输入、动画等)。
Vue 2:通过异步更新队列
数据变化后,DOM 更新不会立即执行,而是缓冲到队列中
在下一个事件循环(microtask)中批量处理,减少 DOM 操作次数
本质是优化更新频率,避免短时间内多次渲染
Vue 3:引入基于 Proxy 的响应式系统 + 时间切片(Time Slicing)
运行时采用类似 Fiber 的策略:将渲染任务分解,利用 requestIdleCallback 在浏览器空闲时执行
支持优先级调度,优先处理用户交互等关键任务
