# fiber 相关基础知识
Fiber 出现的原因 ?
- javascript 执行引擎和渲染引擎在同一个线程,两者互斥;每帧开头包括样式计算、布局和绘制,屏幕刷新率 1s 60帧, 1000/60, js 执行与渲染的间隔是16.6ms,不让一帧的工作量超过16 ms
因为 JavaScript 单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出响应,React 的更新过程就是犯了这个禁忌,而 React Fiber 就是要改变现状。 而可以通过分片来破解 JavaScript 中同步操作时间过长的问题。- 把一个耗时长的任务分成很多小片,分片运行。
React Fiber 把更新过程碎片化,每执行完一段更新过程,就把控制权交还给 React 负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。- 维护每一个分片的数据结构,就是 Fiber。
- 之前的
stack reconciler处理大状态时由于计算和组件树遍历的消耗容易出现渲染线程挂起,进而页面掉帧,所以有Fiber reconciler所替代。
哪些任务需要拆分?哪些任务不需要拆分?
dom Diff过程中的任务可以进行拆分commit 阶段的工作(patch不可拆分
如何进行 Fiber 拆分?
- 实际上就是按虚拟 DOM 节点拆,因为 fiber tree 是根据 vDOM tree 构造出来的,树结构一模一样,只是节点携带的信息有差异。
如何调度任务
- 任务可以分为 2 部分:
工作循环与优先级机制 - 工作循环是基本的
任务调度机制,工作循环中每次处理一个任务(工作单元),处理完毕有一次喘息的机会,此时通过shouldYield函数(idleDeadline.timeRemaining())判读时间是否用完,用完则把时间还给主线程等待下次requestIdleCallback的唤起,否则继续执行任务。 - 优先级机制用来处理
突发事件与优化次序。 有如下策略:- 到 commit 阶段了,提高优先级
- 高优任务做一半出错了,需要降低一下优先级
- 抽空关注一下低优任务,别给饿死了
- 如果对应 DOM 节点此刻不可见,给降到最低优先级
- 任务可以分为 2 部分:
如何中断/断点恢复
- 中断:检查当前正在处理的工作单元,保存当前成果(firstEffect, lastEffect),修改 tag 标记一下,迅速收尾并再开一个 requestIdleCallback,下次有机会再做
- 断点恢复:下次再处理到该工作单元时,看 tag 是被打断的任务,接着做未完成的部分或者重做 自然中断(时间耗尽),或优先级中断(高优任务中断),原理相同。
如何收集任务结果
- 每个节点更新结束时向上归并 effect list 来收集任务结果,reconciliation 结束后,根节点的 effect list 里记录了包括 DOM change 在内的所有 side effect。
- requestIdleCallback 让开发者在主事件循环中执行后台或低优先级的任务,不会对动画和用户交互等关键事件产生影响。
fiber 是模拟了 JS 的调用栈实现的
- Fiber 在 React 生成的 Virtual Dom 基础上增加的一层链表数据结构,把递归遍历转成循环遍历。配合 requestIdleCallback API , 实现任务拆分、中断与恢复。
- JavaScript 用一个栈来管理执行上下文,这个栈中的每一项又包含一个链表。
- 当函数调用时,会入栈一个新的执行上下文,函数调用结束时,执行上下文被出栈。
fiber 架构:
- 循环条件:利用 requestIdeCallback 空闲时间递减.
- 遍历过程:利用链表,找孩子找兄弟找父亲.
fiber 是什么
- Fiber 是一种数据结构,深度优先遍历 虚拟 DOM 节点得到的的链表关系
- 是一个工作单元,每个组件实例和每个DOM节点的
抽象表示都是一个工作单元 - 是一个virtual stack frame,实现了自己的调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。
Fiber 按照虚拟DOM拆分,因为fiber tree是根据Virtual DOM tree 构建出来的,结构上是一样的,只是节点携带的信息不一样。
fiber 执行的大致过程:调度、调和、提交,
- fiberRoot 保存全局调度信息,链表的起点
- scheduler 选择优先级高的任务进入 reconciler
- reconciler 计算变更内容
- react-dom 把变更的内容渲染到页面上
- DOM Diff 时 完成 Reconciliation(协调render):生成Fiber树,得出需要更新的节点信息即找出所有变更(Effect)。这一步是一个渐进的过程,可以被打断。
- Commit(提交)阶段:即需要处理的副作用,将需要更新的节点一次过批量更新,这个过程不能被打断。
- 具体构建过程
Fiber Reconciler在 Diff计算的时候,会生成一棵 Fiber 树,是根据Virtual DOM tree 构建出来的,增加了额外信息,本质上也是一个链表Fiber树在首次渲染的时候会一次生成。在后续需要Diff的时候,会根据已有 Fiber 树和最新 Virtual DOM 的信息,生成一棵新的树,这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程- 在构造Fiber树的过程中,Fiber Reconciler 会将
需要更新的节点信息保存在 Effect List当中,在阶段二执行的时候,会批量更新相应的节点。
Fiber reconciliation的工作循环 过程?
- 找到根节点优先级最高的
workInProgress tree,取其待处理的节点(代表组件或DOM节点) - 检查当前节点是否需要更新,不需要的话,直接到4
- 标记一下(打个tag),更新自己(组件更新props,context等,DOM节点记下DOM change),进行reconcileChildren并返回workInProgress.child
- 不存在workInProgress.child,证明是叶子节点,向上收集effect
- 把child或者sibling当做nextUnitWork,进入下一个工作循环。如果回到了workInProgress tree的根节点,则工作循环结束
- 进入commit阶段
- 找到根节点优先级最高的
Fiber工作阶段phase
diff render和reconciliation 主要是构建workInProgress tree,其实是diff过程- complete diffProperties,
标记tag,收集effect - commit 提交阶段,应用更新
- 调度器(Scheduler)来负责进行任务分配
双缓冲池技术(复用内部对象,节省内存分配、GC 的视觉开销)
- FiberRoot底下的所有节点,都会在算法过程中,尝试创建自己的“镜像”。
- workInProgress tree是reconcile过程中从fiber tree建立的当前进度快照,所有工作都是在这颗树上进行,用于计算更新,完成reconciliation过程。
- 以fiber tree为主,workInProgress tree为辅
- 双缓冲具体指的是workInProgress tree构造完毕,得到的就是新的fiber tree,然后喜新厌旧(把current指针指向workInProgress tree,丢掉旧的fiber tree)就好了
- 在Fiber结构中增加一个alternate字段标识上一次渲染好的Fiber树,下次渲染时可复用
每个fiber上都有个alternate属性,也指向一个fiber,创建workInProgress节点时优先取alternate,没有的话就创建一个?fiber与workInProgress互相持有引用,“喜新厌旧”之后,旧fiber就作为新fiber更新的预留空间,达到复用fiber实例的目的
Fiber的特点
- 暂停工作,并在之后可以返回再次开始;
- 可以为不同类型的工作设置优先级;
- 复用之前已经完成的工作;
- 中止已经不再需要的工作。
- 增量渲染(把渲染任务拆分成块,匀到多帧)
- 更新时能够暂停,终止,复用渲染任务
- 给不同类型的更新赋予优先级
- 并发方面新的基础能力
优先级策略:紧急的事件允许插队
- synchronous 与之前的Stack reconciler操作一样,同步执行;首屏(首次渲染)用,要求尽量快,不管会不会阻塞UI线程
- task 在next tick之前执行----requestIdleCallback回调执行的;
- animation 下一帧之前执行;通过requestAnimationFrame来调度,这样在下一帧就能立即开始动画过程
- high 在不久的将来立即执行----requestIdleCallback回调执行的;
- low 稍微延迟(100-200ms)执行也没关系----requestIdleCallback回调执行的;
- offscreen 下一次render时或scroll时才执行----看不见的
生命周期函数怎么执行?次序的保证?
低优先级饿死现象----尽量复用已完成的操作(reusing work where it can)来缓解
如何实现 Fiber 架构下的 组件渲染 和 副作用收集提交?
- 执行的收集顺序类似于二叉树的先序遍历
- 完成的收集顺序类似于二叉树的后序遍历
React 的 Virtual DOM、Reconciler、Renderer的任务划分
- Virtual DOM 层,描述页面长什么样。
- Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
- Renderer 层,根据不同的平台,渲染出相应的页面
// 主要 fiber 属性,
rootFiber.render()
{
type: xx,
tag: xx,
key: xx,
return: null, // 父亲
sibling: null, //兄弟
child: null, // 大儿子
index: null
effectTag: xx,
nextEffect: xx,
lastEffect: xx,
firstEffect: xx
}
// expiration Time 优先级:0 noWork, 1 never, async 毫秒数越小,优先级越高
// synchronous,与之前的Stack Reconciler操作一样,同步执行
// task,在next tick之前执行
// animation,下一帧之前执行
// high,在不久的将来立即执行
// low,稍微延迟执行也没关系
// offscreen,下一次render时或scroll时才执行
// updateQueue
reconciler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
// Fiber 对应组件的类型 Function/Class/Host...
this.tag = tag;
this.key = key;
// 大部分情况同 type,某些情况不同,比如 FunctionComponent 使用 React.memo 包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于 ClassComponent,指 class,对于 HostComponent,指 DOM 节点 tagName
this.type = null;
// Fiber 对应的真实 DOM 节点
this.stateNode = null;
// 用于连接其他 Fiber 节点形成 Fiber 树
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 保存本次更新会造成的 DOM 操作
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该 fiber 在另一次更新时对应的 fiber
this.alternate = null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48