共计 7039 个字符,预计需要花费 18 分钟才能阅读完成。
前言
本文作为本人深刻学习 React Fiber (Reactv16.8.6
)的了解,本篇仅介绍大抵流程,Fiber 具体源码本文不作细节形容。
本文同步公布在我的 Github 集体博客
Fiber 呈现的背景
首先要晓得的是,JavaScript 引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起期待。
在这样的机制下,如果 JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地期待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿。
而这正是 React 15 的 Stack Reconciler 所面临的问题,即是 JavaScript 对主线程的超时占用问题。Stack Reconciler 是一个同步的递归过程,应用的是 JavaScript 引擎本身的函数调用栈,它会始终执行到栈空为止,所以当 React 在渲染组件时,从开始到渲染实现整个过程是零打碎敲的。如果渲染的组件比拟宏大,js 执行会占据主线程较长时间,会导致页面响应度变差。
而且所有的工作都是依照先后顺序,没有辨别优先级,这样就会导致优先级比拟高的工作无奈被优先执行。
Fiber 是什么
Fiber 的中文翻译叫纤程,与过程、线程同为程序执行过程,Fiber 就是比线程还要细微的一个过程。纤程意在对渲染过程实现进行更加精密的管制。
从架构角度来看,Fiber 是对 React 外围算法(即和谐过程)的重写。
从编码角度来看,Fiber 是 React 外部所定义的一种数据结构,它是 Fiber 树结构的节点单位,也就是 React 16 新架构下的 ” 虚构 DOM”。
一个 fiber 就是一个 JavaScript 对象,Fiber 的数据结构如下:
type Fiber = {
// 用于标记 fiber 的 WorkTag 类型,次要示意以后 fiber 代表的组件类型如 FunctionComponent、ClassComponent 等
tag: WorkTag,
// ReactElement 外面的 key
key: null | string,
// ReactElement.type,调用 `createElement` 的第一个参数
elementType: any,
// The resolved function/class/ associated with this fiber.
// 示意以后代表的节点类型
type: any,
// 示意以后 FiberNode 对应的 element 组件实例
stateNode: any,
// 指向他在 Fiber 节点树中的 `parent`,用来在解决完这个节点之后向上返回
return: Fiber | null,
// 指向本人的第一个子节点
child: Fiber | null,
// 指向本人的兄弟构造,兄弟节点的 return 指向同一个父节点
sibling: Fiber | null,
index: number,
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
// 以后处理过程中的组件 props 对象
pendingProps: any,
// 上一次渲染实现之后的 props
memoizedProps: any,
// 该 Fiber 对应的组件产生的 Update 会寄存在这个队列外面
updateQueue: UpdateQueue<any> | null,
// 上一次渲染的时候的 state
memoizedState: any,
// 一个列表,寄存这个 Fiber 依赖的 context
firstContextDependency: ContextDependency<mixed> | null,
mode: TypeOfMode,
// Effect
// 用来记录 Side Effect
effectTag: SideEffectTag,
// 单链表用来疾速查找下一个 side effect
nextEffect: Fiber | null,
// 子树中第一个 side effect
firstEffect: Fiber | null,
// 子树中最初一个 side effect
lastEffect: Fiber | null,
// 代表工作在将来的哪个工夫点应该被实现,之后版本改名为 lanes
expirationTime: ExpirationTime,
// 疾速确定子树中是否有不在期待的变动
childExpirationTime: ExpirationTime,
// fiber 的版本池,即记录 fiber 更新过程,便于复原
alternate: Fiber | null,
}
在 2020 年 5 月,以 expirationTime 属性为代表的优先级模型被 lanes 取代。
Fiber 如何解决问题的
Fiber 把一个渲染工作合成为多个渲染工作,而不是一次性实现,把每一个宰割得很细的工作视作一个 ” 执行单元 ”,React 就会查看当初还剩多少工夫,如果没有工夫就将控制权让进来,故工作会被扩散到多个帧外面,两头能够返回至主过程管制执行其余工作,最终实现更晦涩的用户体验。
即是实现了 ” 增量渲染 ”,实现了可中断与复原,复原后也能够复用之前的中间状态,并给不同的工作赋予不同的优先级,其中每个工作更新单元为 React Element 对应的 Fiber 节点。
Fiber 实现原理
实现的形式是 requestIdleCallback
这一 API,但 React 团队 polyfill 了这个 API,使其比照原生的浏览器兼容性更好且拓展了个性。
window.requestIdleCallback()
办法将在浏览器的闲暇时段内调用的函数排队。这使开发者可能在主事件循环上执行后盾和低优先级工作,而不会影响提早要害事件,如动画和输出响应。函数个别会按先进先调用的程序执行,然而,如果回调函数指定了执行超时工夫 timeout,则有可能为了在超时前执行函数而打乱执行程序。
requestIdleCallback
回调的执行的前提条件是以后浏览器处于闲暇状态。
即 requestIdleCallback
的作用是在浏览器一帧的残余闲暇工夫内执行优先度绝对较低的工作。首先 React 中工作切割为多个步骤,分批实现。在实现一部分工作之后,将控制权交回给浏览器,让浏览器有工夫再进行页面的渲染。等浏览器忙完之后有剩余时间,再持续之前 React 未实现的工作,是一种单干式调度。
简而言之,由浏览器给咱们调配执行工夫片,咱们要依照约定在这个工夫内执行结束,并将控制权还给浏览器。
React 16 的 Reconciler
基于 Fiber 节点实现,被称为 Fiber Reconciler。
作为动态的数据结构来说,每个 Fiber 节点对应一个 React element,保留了该组件的类型(函数组件 / 类组件 / 原生组件等等)、对应的 DOM 节点等信息。
作为动静的工作单元来说,每个 Fiber 节点保留了本次更新中该组件扭转的状态、要执行的工作。
每个 Fiber 节点有个对应的 React element,多个 Fiber 节点是如何连贯造成树呢?靠如下三个属性:
// 指向父级 Fiber 节点
this.return = null
// 指向子 Fiber 节点
this.child = null
// 指向左边第一个兄弟 Fiber 节点
this.sibling = null
Fiber 架构外围
Fiber 架构能够分为三层:
- Scheduler 调度器 —— 调度工作的优先级,高优工作优先进入 Reconciler
- Reconciler 协调器 —— 负责找出变动的组件
- Renderer 渲染器 —— 负责将变动的组件渲染到页面上
相比 React15,React16 多了Scheduler(调度器),调度器的作用是调度更新的优先级。
在新的架构模式下,工作流如下:
- 每个更新工作都会被赋予一个优先级。
- 当更新工作到达调度器时,高优先级的更新工作(记为 A)会更快地被调度进 Reconciler 层;
- 此时若有新的更新工作(记为 B)到达调度器,调度器会查看它的优先级,若发现 B 的优先级高于当前任务 A,那么以后处于 Reconciler 层的 A 工作就会被中断,调度器会将 B 工作推入 Reconciler 层。
- 当 B 工作实现渲染后,新一轮的调度开始,之前被中断的 A 工作将会被从新推入 Reconciler 层,持续它的渲染之旅,即“可复原”。
Fiber 架构的外围即是 ” 可中断 ”、” 可复原 ”、” 优先级 ”
Scheduler 调度器
这个须要下面提到的requestIdleCallback
,React 团队实现了性能更齐备的 requestIdleCallback
polyfill,这就是 Scheduler。除了在闲暇时触发回调的性能外,Scheduler 还提供了多种调度优先级供工作设置。
Reconciler 协调器
在 React 15 中是递归解决虚构 DOM 的,React 16 则是变成了能够中断的循环过程,每次循环都会调用 shouldYield
判断以后是否有剩余时间。
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
// workInProgress 示意以后工作进度的树。workInProgress = performUnitOfWork(workInProgress)
}
}
React 16 是如何解决中断更新时 DOM 渲染不齐全的问题呢?
在 React 16 中,Reconciler
与 Renderer
不再是交替工作。当 Scheduler
将工作交给 Reconciler
后,Reconciler
会为变动的虚构 DOM 打上的标记。
export const Placement = /* */ 0b0000000000010
export const Update = /* */ 0b0000000000100
export const PlacementAndUpdate = /* */ 0b0000000000110
export const Deletion = /* */ 0b0000000001000
Placement
示意插入操作PlacementAndUpdate
示意替换操作Update
示意更新操作Deletion
示意删除操作
整个 Scheduler
与Reconciler
的工作都在内存中进行,所以即便重复中断,用户也不会看见更新不齐全的 DOM。只有当所有组件都实现 Reconciler
的工作,才会对立交给Renderer
。
Renderer 渲染器
Renderer
依据 Reconciler
为虚构 DOM 打的标记,同步执行对应的 DOM 操作。
Fiber 架构对生命周期的影响
- render 阶段:污浊且没有副作用,可能会被 React 暂停、终止或重新启动。
- pre-commit 阶段:能够读取 DOM。
- commit 阶段:能够应用 DOM,运行副作用,安顿更新。
其中 pre-commit 和 commit 从大阶段上来看都属于 commit 阶段。
在 render 阶段,React 次要是在内存中做计算,明确 DOM 树的更新点;而 commit 阶段,则负责把 render 阶段生成的更新真正地执行掉。
新老两种架构对 React 生命周期的影响次要在 render 这个阶段,这个影响是通过减少 Scheduler 层和改写 Reconciler 层来实现的。
在 render 阶段,一个宏大的更新工作被合成为了一个个的工作单元,这些工作单元有着不同的优先级,React 能够依据优先级的高下去实现工作单元的打断和复原。
之前写过一篇文章对于为什么 React 一些旧生命周期函数打算废除的起因:谈谈对 React 新旧生命周期的了解
而这次从 Firber 机制 render 阶段的角度看这三个生命周期,这三个生命周期的独特特点是都处于 render 阶段:
componentWillMount
componentWillUpdate
componentWillReceiveProps
因为 render 阶段是容许暂停、终止和重启的,这就导致 render 阶段的生命周期都有可能被反复执行,故也是废除他们的起因之一。
Fiber 更新过程
虚构 DOM 更新过程分为 2 个阶段:
- render/reconciliation 协调阶段(可中断 / 异步):通过 Diff 算法找出所有节点变更,例如节点新增、删除、属性变更等等, 取得须要更新的节点信息,对应晚期版本的 Diff 过程。
- commit 提交阶段(不可中断 / 同步):将须要更新的节点一次过批量更新,对应晚期版本的 patch 过程。
协调阶段
在协调阶段会进行 Diff 计算,会生成一棵 Fiber 树。
该阶段开始于 performSyncWorkOnRoot
或performConcurrentWorkOnRoot
办法的调用。这取决于本次更新是同步更新还是异步更新。
// performSyncWorkOnRoot 会调用该办法
function workLoopSync() {while (workInProgress !== null) {performUnitOfWork(workInProgress)
}
}
// performConcurrentWorkOnRoot 会调用该办法
function workLoopConcurrent() {while (workInProgress !== null && !shouldYield()) {performUnitOfWork(workInProgress)
}
}
它们惟一的区别是是否调用 shouldYield
。如果以后浏览器帧没有剩余时间,shouldYield
会停止循环,直到浏览器有闲暇工夫后再持续遍历。
workInProgress
代表以后已创立的 workInProgress fiber。
performUnitOfWork
办法将触发对 beginWork
的调用,进而实现对新 Fiber 节点的创立。若 beginWork
所创立的 Fiber 节点不为空,则 performUniOfWork
会用这个新的 Fiber 节点来更新 workInProgress
的值,为下一次循环做筹备。
通过循环调用 performUnitOfWork
来触发 beginWork
,新的 Fiber 节点就会被一直地创立。当 workInProgress
终于为空时,阐明没有新的节点能够创立了,也就意味着曾经实现对整棵 Fiber 树的构建。
咱们晓得 Fiber Reconciler 是从 Stack Reconciler 重构而来,通过遍历的形式实现可中断的递归,所以 performUnitOfWork
的工作能够分为两局部:” 递 ” 和 ” 归 ”。
“ 递阶段 ”
首先从 rootFiber 开始向下深度优先遍历。为遍历到的每个 Fiber 节点调用 beginWork
办法。
function beginWork(
current: Fiber | null, // 以后组件对应的 Fiber 节点在上一次更新时的 Fiber 节点
workInProgress: Fiber, // 以后组件对应的 Fiber 节点
renderExpirationTime: ExpirationTime // 优先级相干
): Fiber | null {// ... 省略函数体}
该办法会依据传入的 Fiber 节点创立子 Fiber 节点,并将这两个 Fiber 节点连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入 ” 归 ” 阶段。
“ 归阶段 ”
在 ” 归 ” 阶段会调用 completeWork
解决 Fiber 节点。
completeWork 将依据 workInProgress 节点的 tag 属性的不同,进入不同的 DOM 节点的创立、解决逻辑。
completeWork 外部有 3 个要害动作:
- 创立 DOM 节点(CreateInstance)
- 将 DOM 节点插入到 DOM 树中(AppendAllChildren)
- 为 DOM 节点设置属性(FinalizeInitialChildren)
当某个 Fiber 节点执行完completeWork
,如果其存在兄弟 Fiber 节点(即fiber.sibling !== null
),会进入其兄弟 Fiber 的 ” 递 ” 阶段。
如果不存在兄弟 Fiber,会进入父级 Fiber 的 ” 归 ” 阶段。
“ 递 ” 和 ” 归 ” 阶段会交织执行直到 ” 归 ” 到 rootFiber。至此,协调阶段的工作就完结了。
commit 提交阶段
commit 阶段的次要工作(即 Renderer 的工作流程)分为三局部:
- before mutation 阶段,这个阶段 DOM 节点还没有被渲染到界面下来,过程中会触发
getSnapshotBeforeUpdate
,也会解决useEffect
钩子相干的调度逻辑。 - mutation 阶段,这个阶段负责 DOM 节点的渲染。在渲染过程中,会遍历 effectList,依据 flags(effectTag)的不同,执行不同的 DOM 操作。
- layout 阶段,这个阶段解决 DOM 渲染结束之后的收尾逻辑。比方调用
componentDidMount/componentDidUpdate
,调用useLayoutEffect
钩子函数的回调等。除了这些之外,它还会把 fiberRoot 的 current 指针指向 workInProgress Fiber 树。
参考:
- 修言 - 深入浅出搞定 React
- React 技术揭秘
我近期会保护的开源我的项目:
- 基于 React + TypeScript + Dumi + Jest + Enzyme 开发 UI 组件库
- Next.js 企业级我的项目脚手架模板
- 集体技术博文 Github 仓库
感觉不错的话欢送 star,给我一点激励持续写作吧~