社区精选 | React Fiber 架构原理剖析
今天小编为大家带来的是社区作者 xiangzhihong 的文章,在这篇文章中他剖析详解了 React Fiber 的架构原理。
让我们一起学习吧~
一、概述
在 React 16 之前,VirtualDOM 的更新采用的是Stack架构实现的,也就是循环递归方式。不过,这种对比方式有明显的缺陷,就是一旦任务开始进行就无法中断,如果遇到应用中组件数量比较庞大,那么VirtualDOM 的层级就会比较深,带来的结果就是主线程被长期占用,进而阻塞渲染、造成卡顿现象。
为了避免出现卡顿等问题,我们必须保障在执行更新操作时计算时不能超过16ms,如果超过16ms,就需要先暂停,让给浏览器进行渲染,后续再继续执行更新计算。而Fiber架构就是为了支持“可中断渲染”而创建的。
在React中,Fiber使用了一种新的数据结构fiber tree,它可以把虚拟dom tree转换成一个链表,然后再执行遍历操作,而链表在执行遍历操作时是支持断点重启的,示意图如下。
二、Fiber架构
2.1 执行单元
官方介绍中,Fiber 被理解为是一种数据结构,但是我们也可以将它理解为是一个执行单元。
Fiber 可以理解为一个执行单元,每次执行完一个执行单元,React Fiber就会检查还剩多少时间,如果没有时间则将控制权让出去,然后由浏览器执行渲染操作。React Fiber 与浏览器的交互流程如下图。
可以看到,React 首先向浏览器请求调度,浏览器在执行完一帧后如果还有空闲时间,会去判断是否存在待执行任务,不存在就直接将控制权交给浏览器;如果存在就会执行对应的任务,执行完一个新的任务单元之后会继续判断是否还有时间,有时间且有待执行任务则会继续执行下一个任务,否则将控制权交给浏览器执行渲染,这个流程是循环进行的。
所以,我们可以将Fiber 理解为一个执行单元,并且这个执行单元必须是一次完成的,不能出现暂停。并且,这个小的执行单元在执行完后计算之后,可以移交控制权给浏览器去响应用户,从而提升了渲染的效率。
2.2 数据结构
在官方的文档中,Fiber 被解释为是一种数据结构,即链表结构。在链表结构中,每个 Virtual DOM 都可以表示为一个 fiber,如下图所示。
通常,一个 fiber包括了 child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性,React Fiber 机制的实现,就是依赖于上面的数据结构。
2.3 Fiber链表结构
通过介绍,我们知道Fiber使用的是链表结构,准确的说是单链表树结构,详见ReactFiber.js源码。为了放便理解 Fiber 的遍历过程,下面我们就看下Fiber链表结构。
在上面的例子中,每一个单元都包含了payload(数据)和nextUpdate(指向下一个单元的指针)两个元素,定义结构如下:
class Update {
constructor(payload, nextUpdate) {
this.payload = payload //payload 数据
this.nextUpdate = nextUpdate //指向下一个节点的指针
}
}
接下来定义一个队列,把每个单元串联起来。为此,我们需要定义两个指针:头指针firstUpdate和尾指针lastUpdate,作用是指向第一个单元和最后一个单元,然后再加入baseState属性存储React中的state状态。
class UpdateQueue {
constructor() {
this.baseState = null // state
this.firstUpdate = null // 第一个更新
this.lastUpdate = null // 最后一个更新
}
}
接下来,再定义两个方法:用于插入节点单元的enqueueUpdate()和用于更新队列的forceUpdate()。并且,插入节点单元时需要考虑是否已经存在节点,如果不存在直接将firstUpdate、lastUpdate指向此节点即可。更新队列是遍历这个链表,根据payload中的内容去更新state的值
class UpdateQueue {
//.....
enqueueUpdate(update) {
// 当前链表是空链表
if (!this.firstUpdate) {
this.firstUpdate = this.lastUpdate = update
} else {
// 当前链表不为空
this.lastUpdate.nextUpdate = update
this.lastUpdate = update
}
}
// 获取state,然后遍历这个链表,进行更新
forceUpdate() {
let currentState = this.baseState || {}
let currentUpdate = this.firstUpdate
while (currentUpdate) {
// 判断是函数还是对象,是函数则需要执行,是对象则直接返回
let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
currentState = { ...currentState, ...nextState }
currentUpdate = currentUpdate.nextUpdate
}
// 更新完成后清空链表
this.firstUpdate = this.lastUpdate = null
this.baseState = currentState
return currentState
}
最后,我们写一个测试的用例:实例化一个队列,向其中加入很多节点,再更新这个队列。
let queue = new UpdateQueue()
queue.enqueueUpdate(new Update({ name: 'www' }))
queue.enqueueUpdate(new Update({ age: 10 }))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.enqueueUpdate(new Update(state => ({ age: state.age + 1 })))
queue.forceUpdate()
console.log(queue.baseState); //输出{ name:'www',age:12 }
2.4 Fiber节点
Fiber 框架的拆分单位是 fiber(fiber tree上的一个节点),实际上拆分的节点就是虚拟DOM的节点,我们需要根据虚拟dom去生成 fiber tree。Fiber节点的数据结构如下:
{
type: any, //对于类组件,它指向构造函数;对于DOM元素,它指定HTML tag
key: null | string, //唯一标识符
stateNode: any, //保存对组件的类实例,DOM节点或与fiber节点关联的其他React元素类型的引用
child: Fiber | null, //大儿子
sibling: Fiber | null, //下一个兄弟
return: Fiber | null, //父节点
tag: WorkTag, //定义fiber操作的类型, 详见https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactWorkTags.js
nextEffect: Fiber | null, //指向下一个节点的指针
updateQueue: mixed, //用于状态更新,回调函数,DOM更新的队列
memoizedState: any, //用于创建输出的fiber状态
pendingProps: any, //已从React元素中的新数据更新,并且需要应用于子组件或DOM元素的props
memoizedProps: any, //在前一次渲染期间用于创建输出的props
// ……
最终, 所有的fiber 节点通过以下属性:child,sibling 和 return来构成一个树链表。
其他的属性还有memoizedState(创建输出的 fiber 的状态)、pendingProps(将要改变的 props )、memoizedProps(上次渲染创建输出的 props )、pendingWorkPriority(定义 fiber 工作优先级)等等就不在过多的介绍了。
2.5 API
2.5.1 requestAnimationFrame
requestAnimationFrame是浏览器提供的绘制动画的 API ,它要求浏览器在下次重绘之前(即下一帧)调用指定的回调函数以更新动画。
例如,使用requestAnimationFrame实现正方形的宽度加1px,直到宽度达到100px停止,代码如下。
<body>
<div id="div" class="progress-bar "></div>
<button id="start">开始动画</button>
</body>
<script>
let btn = document.getElementById('start')
let div = document.getElementById('div')
let start = 0
let allInterval = []
const progress = () => {
div.style.width = div.offsetWidth + 1 + 'px'
div.innerHTML = (div.offsetWidth) + '%'
if (div.offsetWidth < 100) {
let current = Date.now()
allInterval.push(current - start)
start = current
requestAnimationFrame(progress)
}
}
btn.addEventListener('click', () => {
div.style.width = 0
let currrent = Date.now()
start = currrent
requestAnimationFrame(progress)
})
</script>
运行上面的代码,就可以看到浏览器会在每一帧运行结束后,将div的宽度加1px,直到100px为止。
2.5.2 requestIdleCallback
requestIdleCallback 也是 Fiber 的基础 API 。requestIdleCallback能使开发者在主事件循环上执行后台和低优先级的工作,而不会影响延迟关键事件,如动画和输入响应。正常帧任务完成后没超过16ms,说明有多余的空闲时间,此时就会执行requestIdleCallback里注册的任务。
具体的执行流程是,开发者采用requestIdleCallback方法注册对应的任务,告知浏览器任务的优先级不高,如果每一帧内存在空闲时间,就可以执行注册的这个任务。另外,开发者是可以传入timeout参数去定义超时时间的,如果到了超时时间,那么浏览器必须立即执行,使用方法如下:
window.requestIdleCallback(callback, { timeout: 1000 })
浏览器执行完方法后,如果没有剩余时间了,或者已经没有下一个可执行的任务了,React应该归还控制权,并同样使用requestIdleCallback去申请下一个时间片。具体的流程如下图:
其中,requestIdleCallback的callback中会接收到默认参数 deadline ,其中包含了以下两个属性:
timeRamining:返回当前帧还剩多少时间供用户使用。
didTimeout:返回 callback 任务是否超时。
三、Fiber执行流程
3.1 render阶段
3.1.1 遍历流程
从顶点开始遍历;
如果有大儿子,先遍历大儿子;如果没有大儿子,则表示遍历完成;
大儿子:a. 如果有弟弟,则返回弟弟,跳到2 b. 如果没有弟弟,则返回父节点,并标志完成父节点遍历,跳到2 d. 如果没有父节点则标志遍历结束
const A1 = { type: 'div', key: 'A1' }
const B1 = { type: 'div', key: 'B1', return: A1 }
const B2 = { type: 'div', key: 'B2', return: A1 }
const C1 = { type: 'div', key: 'C1', return: B1 }
const C2 = { type: 'div', key: 'C2', return: B1 }
const C3 = { type: 'div', key: 'C3', return: B2 }
const C4 = { type: 'div', key: 'C4', return: B2 }
A1.child = B1
B1.sibling = B2
B1.child = C1
C1.sibling = C2
B2.child = C3
C3.sibling = C4
module.exports = A1
3.1.2 收集effect list
如果当前节点需要更新,则打tag更新当前节点状态(props, state, context等);
为每个子节点创建fiber。如果没有产生child fiber,则结束该节点,把effect list归并到return,把此节点的sibling节点作为下一个遍历节点;否则把child节点作为下一个遍历节点;
如果有剩余时间,则开始下一个节点,否则等下一次主线程空闲再开始下一个节点;
如果没有下一个节点了,进入pendingCommit状态,此时effect list收集完毕,结束。
const reconcileChildren = (currentFiber, newChildren) => {
let newChildIndex = 0
let prevSibling // 上一个子fiber
// 遍历子虚拟DOM元素数组,为每个虚拟DOM元素创建子fiber
while (newChildIndex < newChildren.length) {
let newChild = newChildren[newChildIndex]
let tag
// 打tag,定义 fiber类型
if (newChild.type === ELEMENT_TEXT) { // 这是文本节点
tag = TAG_TEXT
} else if (typeof newChild.type === 'string') { // 如果type是字符串,则是原生DOM节点
tag = TAG_HOST
}
let newFiber = {
tag,
type: newChild.type,
props: newChild.props,
stateNode: null, // 还未创建DOM元素
return: currentFiber, // 父亲fiber
effectTag: INSERT, // 副作用标识,包括新增、删除、更新
nextEffect: null, // 指向下一个fiber,effect list通过nextEffect指针进行连接
}
if (newFiber) {
if (newChildIndex === 0) {
currentFiber.child = newFiber // child为大儿子
} else {
prevSibling.sibling = newFiber // 让大儿子的sibling指向二儿子
}
prevSibling = newFiber
}
newChildIndex++
}
}
firstEffect:指向第一个有副作用的子fiber。
lastEffect:指向最后一个有副作用的子fiber。
// 在完成的时候要收集有副作用的fiber,组成effect list
const completeUnitOfWork = (currentFiber) => {
// 后续遍历,儿子们完成之后,自己才能完成。最后会得到以上图中的链条结构。
let returnFiber = currentFiber.return
if (returnFiber) {
// 如果父亲fiber的firstEffect没有值,则将其指向当前fiber的firstEffect
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect
}
// 如果当前fiber的lastEffect有值
if (currentFiber.lastEffect) {
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect
}
returnFiber.lastEffect = currentFiber.lastEffect
}
const effectTag = currentFiber.effectTag
if (effectTag) { // 说明有副作用
// 每个fiber有两个属性:
// 1)firstEffect:指向第一个有副作用的子fiber
// 2)lastEffect:指向最后一个有副作用的子fiber
// 中间的使用nextEffect做成一个单链表
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber
} else {
returnFiber.firstEffect = currentFiber
}
returnFiber.lastEffect = currentFiber
}
}
}
const performUnitOfWork = (currentFiber) => {
beginWork(currentFiber)
if (currentFiber.child) {
return currentFiber.child
}
while (currentFiber) {
completeUnitOfWork(currentFiber)
if (currentFiber.sibling) {
return currentFiber.sibling
}
currentFiber = currentFiber.return
}
}
3.2 commit阶段
3.2.1 根据effect list 更新视图
const commitWork = currentFiber => {
if (!currentFiber) return
let returnFiber = currentFiber.return
let returnDOM = returnFiber.stateNode // 父节点元素
if (currentFiber.effectTag === INSERT) { // 如果当前fiber的effectTag标识位INSERT,则代表其是需要插入的节点
returnDOM.appendChild(currentFiber.stateNode)
} else if (currentFiber.effectTag === DELETE) { // 如果当前fiber的effectTag标识位DELETE,则代表其是需要删除的节点
returnDOM.removeChild(currentFiber.stateNode)
} else if (currentFiber.effectTag === UPDATE) { // 如果当前fiber的effectTag标识位UPDATE,则代表其是需要更新的节点
if (currentFiber.type === ELEMENT_TEXT) {
if (currentFiber.alternate.props.text !== currentFiber.props.text) {
currentFiber.stateNode.textContent = currentFiber.props.text
}
}
}
currentFiber.effectTag = null
}
/**
* 根据一个 fiber 的 effect list 更新视图
*/
const commitRoot = () => {
let currentFiber = workInProgressRoot.firstEffect
while (currentFiber) {
commitWork(currentFiber)
currentFiber = currentFiber.nextEffect
}
currentRoot = workInProgressRoot // 把当前渲染成功的根fiber赋给currentRoot
workInProgressRoot = null
}
3.2.2 视图更新
const workloop = (deadline) => {
let shouldYield = false // 是否需要让出控制权
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
shouldYield = deadline.timeRemaining() < 1 // 如果执行完任务后,剩余时间小于1ms,则需要让出控制权给浏览器
}
if (!nextUnitOfWork && workInProgressRoot) {
console.log('render阶段结束')
commitRoot() // 没有下一个任务了,根据effect list结果批量更新视图
}
// 请求浏览器进行再次调度
requestIdleCallback(workloop, { timeout: 1000 })
}
四、总结
关注公众号:拾黑(shiheibook)了解更多
赞助链接:
关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/
随时掌握互联网精彩
- ARC浏览器 - 让人爱不释手替代 Chrome 的 “最新爆火” AI 浏览器
- 日元对人民币汇率2024年3月27日
- 【杂谈快报】字节开启新一轮回购 :估值约2680亿美元,上市计划仍未定;阿里将首次派发年度股息,总额近179亿
- 澳元兑换人民币汇率2023年8月11日
- 大公司创新情报(11月26日):蚂蚁集团发布多款数字科技产品 助力菲律宾数字化转型
- 办公转型,时不我待
- 国产剧中的姐弟CP,仿佛早年霸总的“性转版”
- 2021年上半年海洋领域世界科技发展趋势
- 上新了!微软 Surface 商用系列又添新成员
- 美国半导体行业协会:提升半导体供应链安全不能靠“自给自足”
- 那些骂苹果不送充电器的人,正在狂买iPhone12
- 教你打开线程、进程和协程的大门!