什么是fiber
Fiber 是React 16 中采用的新协调(reconciliation)引擎,主要目标是支持虚拟DOM 的渐进式渲染,它是对 React 协调算法的完全重写,以解决 React 中一些长期存在的问题。 因为 Fiber 是异步的,React 可以:
- 随着新更新的到来,暂停、恢复和重新启动组件上的渲染工作
- 重复使用以前完成的工作,如果不需要甚至中止它
- 将工作分成块并根据重要性确定任务的优先级
为什么会出现fiber
react18之前的协调算法
~~在 React 中有一个过程被称之为 协调 reconciliation 。也就是当我们调用 setState 方法,或是框架检测到 state or props变化后,它执行遍历并通过将新树与渲染树进行比较来确定树中发生了什么变化。然后,它将这些更改应用于当前树,从而更新与调用对应的状态。
fiber出现之前的协调算法是一种纯递归算法。更新会导致整个子树立即重新渲染,这样做的弊端是可能会造成浪费,导致掉帧并降低用户体验。
下图展示了setstate 一帧之内react做的事情
~~
计算机显示器只不过是一本自动翻书,当屏幕上的内容发生变化时会不断播放。
现在大多数设备以 60 FPS 刷新屏幕,1/60 = 16.67 毫秒,这意味着每 16 毫秒显示一个新帧。这个数字很重要,因为如果 React 渲染器在屏幕上渲染某些东西的时间超过 16 毫秒,浏览器就会丢弃该帧。
App如果 React 协调算法这一帧 的时间超过 16ms,就会丢帧。
每次有 state 的变化 React 重新计算,如果计算量过大,浏览器主线程来不及做其他的事情,比如 rerender 或者 layout,那例如动画就会出现[卡顿现象]~~~~
JavaScript 是单线程运行的,所以JavaScript 线程和渲染线程是互斥的:这两个线程不能够穿插执行,必须串行。当其中一个线程执行时,另一个线程只能挂起等待。在这样的机制下,若 JavaScript 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,带给用户的体验就是所谓的“卡顿” React V15 在渲染时,会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程 React 称为 Reconcilation,这个递归比较找出变动结果的过程是不能被打断的,如果这个过程比较长就导致JS一直占用浏览器主线程,渲染线程得不到执行就会造成页面的卡顿 [浏览器线程有哪些](www.yuque.com/kongliyuan/…)
fiber是怎么做的?
- 为不同类型的工作分配优先级
- 暂停工作,稍后再回来
- 如果不再需要,则中止工作
- 重用以前完成的工作
Fiber 可以被理解为划分一个个更小的执行单元,它是把一个大任务拆分为了很多个小块任务,一个小块任务的执行必须是一次完成的,不能出现暂停,但是一个小块任务执行完后可以移交控制权给浏览器去响应用户,每次执行完一个执行单元,react 就会检查现在还剩多少时间,如果没有时间则将控制权让出去,从而不用像之前一样要等那个大任务一直执行完成再去响应用户。之后被打断的任务将会使用requestIdleCallback调度,这个API可以让任务在浏览器空闲的时机执行 这里的拆分主要是对找出要更新的节点这一步骤的拆分,这一步骤是可以打断的,这一步骤完成之后会开始将更新的fiber树转换成dom 渲染成视图,这一步骤是不可以被打断的,否则就会造成页面的混乱
requestIdlecallback requestAnimationFrame的区别
fiber中常涉及的概念
- workInProgress:循环处理每个 fiber 节点的时候,有个指针记录着当前的 fiber 节点,叫做 workInProgress。
- workInProgressHook,它通过记录当前生成(更新)的hook对象,可以间接反映在组件中当前调用到哪个hook函数了。每调用一次hook函数,就将这个指针的指向移到该hook函数产生的hook对象上
fiber的实现
相比之前的协调算法,数据结构有啥变化?
Fiber 和函数执行栈一样, 保存了节点处理的上下文信息,因为是手动实现的,所以更为可控,我们可以保存在内存中,随时中断和恢复。
有了这个数据结构调整,现在可以以迭代的方式来处理这些节点了。来看看 performUnitOfWork 的实现, 它其实就是一个深度优先的遍历:
/**
* @params fiber 当前需要处理的节点
* @params topWork 本次更新的根节点
*/
function performUnitOfWork(fiber: Fiber, topWork: Fiber) {
// 对该节点进行处理
beginWork(fiber);
// 如果存在子节点,那么下一个待处理的就是子节点
if (fiber.child) {
return fiber.child;
}
// 没有子节点了,上溯查找兄弟节点
let temp = fiber;
while (temp) {
completeWork(temp);
// 到顶层节点了, 退出
if (temp === topWork) {
break
}
// 找到,下一个要处理的就是兄弟节点
if (temp.sibling) {
return temp.sibling;
}
// 没有, 继续上溯
temp = temp.return;
}
}
Fiber 就是我们所说的工作单元,performUnitOfWork 负责对 Fiber 进行操作,并按照深度遍历的顺序返回下一个 Fiber。
因为使用了链表结构,即使处理流程被中断了,我们随时可以从上次未处理完的Fiber继续遍历下去。
采用链表实现。每个 Virtual DOM节点 都可以表示为一个 fiber
fiber节点的组成
fiber 节点包括了以下的属性: (1)type & key
fiber 的 type 和 key 与 React 元素的作用相同。fiber 的 type 描述了它对应的组件,对于复合组件,type 是函数或类组件本身。对于原生标签(div,span等),type 是一个字符串。随着 type 的不同,在 reconciliation 期间使用 key 来确定 fiber 是否可以重新使用。
(2)stateNode 节点实例(状态) 对于宿主组件,这里保存宿主组件的实例, 例如DOM节点。 对于类组件来说,这里保存类组件的实例 对于函数组件说,这里为空,因为函数组件没有实例
(3)child & sibling & return
child 属性指向此节点的第一个子节点(大儿子)。
sibling 属性指向此节点的下一个兄弟节点(大儿子指向二儿子、二儿子指向三儿子)。
return 属性指向此节点的父节点,即当前节点处理完毕后,应该向谁提交自己的成果。如果 fiber 具有多个子 fiber,则每个子 fiber 的 return fiber 是 parent 。
(4)pendingWorkPriority
pendingWorkPriority是一个数字,表示fiber所代表的工作的优先级
(5)pendingProps 新的、待处理的props
(6) memoizedProps memoizedState 上一次渲染的props,上一次渲染的组件状态
每次重新渲染之后会经历哪些阶段?
在此之前都是一边Diff一边提交的。fiber的阶段:
- ⚛️ 协调阶段: 虚拟dom->fiber树的阶段
可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更React 称之为'副作用(Effect)' . 以下生命周期钩子会在协调阶段被调用:
- `constructor`
- `componentWillMount` 废弃
- `componentWillReceiveProps` 废弃
- `static getDerivedStateFromProps`
- `shouldComponentUpdate`
- `componentWillUpdate` 废弃
- `render`
- ⚛️ 提交阶段: fiber-> dom
将上一个阶段计算出来的需要处理的**副作用(Effects)**一次性执行了。这个阶段必须同步执行,不能被打断. 这些生命周期钩子在提交阶段被执行:
- `getSnapshotBeforeUpdate()` 严格来说,这个是在进入 commit 阶段前调用
- `componentDidMount`
- `componentDidUpdate`
- `componentWillUnmount`
为什么will被废弃了?
因为协调阶段可能被中断、恢复,甚至重做,⚠️React 协调阶段的生命周期钩子可能会被调用多次! , 例如 componentWillMount 可能会被调用两次
也就是说,在协调阶段如果时间片用完,React就会选择让出控制权。因为协调阶段执行的工作不会导致任何用户可见的变更,所以在这个阶段让出控制权不会有什么问题。
怎么调度的?
假设用户调用 setState 更新组件, 这个待更新的任务会先放入队列中, 然后通过 requestIdleCallback 请求浏览器调度:
updateQueue.push(updateTask);
requestIdleCallback(performWork, {timeout});
现在浏览器有空闲或者超时了就会调用performWork来执行任务:
// 1️⃣ performWork 会拿到一个Deadline,表示剩余时间
function performWork(deadline) {
// 2️⃣ 循环取出updateQueue中的任务
while (updateQueue.length > 0 && deadline.timeRemaining() > ENOUGH_TIME) {
workLoop(deadline);
}
// 3️⃣ 如果在本次执行中,未能将所有任务执行完毕,那就再请求浏览器调度
if (updateQueue.length > 0) {
requestIdleCallback(performWork);
}
}
常见问题
fiber节点跟jsx节点有啥区别?
vdom树跟fiber树的区别
fiber树:
vdom树
可以看到的是,相比vdom树,fiber树还会通过sibling指向自己的兄弟节点,并且所有的节点可以 return 到父节点。这就是为什么可以做到恢复上一次中断的执行单元,如果按照之前的结构,要恢复递归现场,可能需要从头开始, 恢复到之前的调用栈。 数据结构的转变:树->链表树 为什么说是单链表? child、return是用来构建“树”,“sibling”用来构建同级元素的“单向”链表,通过遍历这个“单向”链表来 dom diff。
为什么jsx不直接转换成dom了而是要先转换成fiber树?
之前我们是递归渲染 vdom 的,然后 diff 下来做 patch 的渲染:
渲染和 diff 是递归进行的。
现在变成了这样:
注意上图中涉及到reconcile以及commit两个阶段。
意义就在于这个reconcile的可打断上。因为递归渲染 vdom 可能耗时很多,JS 计算量大了会阻塞渲染,而 fiber 是可打断的,就不会阻塞渲染,而且还会在这个过程中把需要用到的 dom 创建好,做好 diff 来确定是增是删还是改。
简单还原fiber的两个阶段,这里以新增节点为例
简单实现reconcile
先实现一下调度 循环处理所有的fiber节点的reconcile,注意这里的循环是有条件的,当没有空闲时间的时候这个循环会被打断。 这里的空闲时间指的是什么? 当前帧执行完绘制剩余的空闲时间
let shouldYield = false;
while (nextFiberReconcileWork && !shouldYield) {
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
);
shouldYield = deadline.timeRemaining() < 1;
}
至于什么时候继续执行,使用requestIdleCallback调度。由于这个API的兼容性不是很好,react16自己实现了一套调度器,调度器还加上了优先级的机制。 什么是requestIdleCallback?
// nextFiberReconcileWork:当前处理到的 fiber 节点
function workLoop(deadline) {
let shouldYield = false;
while (nextFiberReconcileWork && !shouldYield) {
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextFiberReconcileWork) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
总结:这一步骤react将比较新老树的过程拆分成很多的执行单元,每执行完成一个单元会判断一下还有没有下一个单元,以及有没有空闲时间,如果还有空闲时间接着执行下一个单元,如果没有,控制权交还给浏览器,浏览器做一些比如响应用户输入等的操作,然后继续比较直至完成所有的任务。 实现reconcile,reconcile的过程就是vdom转成fiber的过程
function reconcile(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
// 把之前的 vdom 转成 child、sibling、return 这样串联起来 fiber 链表
reconcileChildren(fiber, fiber.props.children)
}
简单实现commit
每个 fiber 节点的渲染就是按照 child、sibling 的顺序以此插入到 dom 中
总结
React setState之后会经历两个阶段:第一个阶段是协调阶段,这个阶段会找出所有的变更,第二个阶段是commit阶段,会讲第一阶段找出的变更然后同步更新他们,fiber出现前,第一个阶段是不能被打断的,而由于JS线程跟浏览器的渲染线程是互斥的,如果这个阶段执行太久一直占用主线程,渲染线程被阻塞,就会导致卡顿,
而从架构角度来看的fiber就是将第一阶段这个长任务分成了很多个小任务,并且按照优先级排序放入待执行的队列,每个任务有一个规定的执行时间,每执行完一个就去检查一些有无剩余时间,如果有并且还有下一个任务的话就执行下一个,否则的话就将控制权交还给浏览器,如果没有的话就打断,被打断的任务通过request Idle Callback调度,在浏览器空闲的时机执行。
从代码层面来看的fiberReact 内部所定义的一种数据结构,它是 Fiber 树结构的节点单位。一个 fiber 就是一个 JavaScript 对象,包含了元素的信息、该元素的key、类型、对应的dom节点、任务应该被执行完毕的时间节点等等,相比虚拟dom的dom节点,他还通过sibling指向自己的兄弟节点,并且所有的节点可以 return 到父节点。这样的话就可以做到恢复上一次中断的执行单元
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,
expirationTime: ExpirationTime, // 代表任务在未来的哪个时间点应该被完成,不包括他的子树产生的任务