为什么要有Fiber?
首先要知道的是,JavaScript 引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待。
在之前的 React15 以及之前的版本,React 对于虚拟 DOM 是采用递归方式遍历更新的,比如一次更新,就会从应用根部递归更新,递归一旦开始,中途无法中断,随着项目越来越复杂,层级越来越深,导致更新的时间越来越长,这样就会超时占用主线程,界面长时间不更新,会导致页面响应度变差,在交互上的体验就是卡顿。
Fiber解决的问题
Fiber 把一个渲染任务分解为多个渲染任务,而不是一次性完成,把每一个分割得很细的任务视作一个”执行单元”,React 就会检查现在还剩多少时间,如果没有时间就将控制权让出去,故任务会被分散到多个帧里面,中间可以返回至主进程控制执行其他任务,最终实现更流畅的用户体验。
即是实现了”增量渲染”,实现了可中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 React Element 对应的 Fiber 节点。
Fiber的原理
requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态。
即requestIdleCallback的作用是在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务。首先 React 中任务切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React 未完成的任务,是一种合作式调度。
简而言之,由浏览器给我们分配执行时间片,我们要按照约定在这个时间内执行完毕,并将控制权还给浏览器。
调用栈逻辑分层
performSyncWorkOnRoot
开启的正是我们反复强调的render阶段commitRoot
方法开启的则是真实DOM的渲染过程(commit 阶段)
这样可以将performSyncWorkOnRoot
和commitRoot
这2个方法为界,将整个流程划分为三个阶段
- 初始化阶段 (同步的)
- render阶段 (同步,异步)
- commit阶段(同步)
初始化阶段
legacyRenderSubtreeIntoContainer的流程
fiberRoot 与 rootFiber 的区别
fiberRoot 是关联真实DOM的容器节点。
rootFiber 是作为虚拟DOM的根节点存在
unbatchedUpdates做了什么?
unbatchedUpdates调用了 updateConatiner
- 请求当前Fiber节点的lane (优先级)
- 结合lane (优先级)创建当前Fiber节点的update对象,并将其入队
- 调度当前节点(rootFiber)
FiberNode是fiber节点对对应的对象的类型,同时还是当前节点的头部节点。
Render阶段(找更新)(递归)
render阶段开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。
beginWork将创建新的Fiber节点
completeWork则负责将Fiber节点映射为DOM节点
beginWork阶段(自顶向下)(递)
该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。
当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
beginWork开启fiber节点创建过程。
beginWork的入参是一对用alternate连接起来的workInProgress和current节点
beginWork的核心逻辑是根据fiber节点(workInProgress) 的tag属性的不同调用不同的节点创建函数
preparefreshStack重置新的堆栈环境的作用
createWorkInProgress
- createWorkInProgress将调用createFiber
- workInProgress是createFiber方法的返回值
- workInProgress的alternate将指向current
- current的alternate将反过来指向workInProgress
workInProgress就是一个fiber节点
workInProgress节点其实就是current节点(即rootFiber)的副本
workLoopSync 流程
通过while循环反复判断workInProgress是否为空
在不为空的情况下执行performUnitOfWork
performUnitOfWork触发对beginWork的调用进而实现对新Fiber书点的创建
通过循环调用performUnitOfWork来触发beginWork新的Fiber节点就会被不断地创建
workInProgress为空就完成了fiber树的创建
通过调用reconcileChidren方法,生成当前节点的子节点
reconcileChildFibers和mountChildFibers的不同,在于对副作用的处理不同。
ChildReconciler中定义了大量如placeXXX、deleteXXX、 updateXXX、reconcileXXX 等这样的函数,这些函数覆盖了对Fiber节点的创建、增加、删除、修改等动作,将直接或间接地被reconcileChildFibers所调用。
ChildReconciler的返回值是一个名为reconcileChildFibers的函数,这个函数是一个逻辑分发器,它将根据入参的不同,执行不同的Fiber节点操作,最终返回不同的目标Fiber节点。
EffectTag记录副作用的类型,数据获取、订阅或者修改DOM等动作
effectTag的意义是告诉渲染器:我这里需要新增DOM节点
export const Placement = /* */ 0b0000000000010; // 插入节点
export const Update = /* */ 0b0000000000100; // 更新fiber
export const Deletion = /* */ 0b0000000001000; // 删除fiebr
export const Snapshot = /* */ 0b0000100000000; // 快照
export const Passive = /* */ 0b0001000000000; // useEffect的副作用
export const Callback = /* */ 0b0000000100000; // setState的 callback
export const Ref = /* */ 0b0000010000000; // ref
beginWork触发的调用流程
fiber节点如何建立起关联的
不同的Fiber节点之间将通过child、return、 sibling 这3个属性建立关系
- return: 指向父级 Fiber 节点。
- child: 指向子 Fiber 节点。
- sibling:指向兄弟 fiber 节点。
completeWork阶段(自底向上执行)(归)
completeWork的工作内容:负责处理Fiber节点到DOM节点的映射逻辑
completeWork内部有3个关键动作:
- 创建DOM节点(Createlnstance)
- 将DOM节点插入到DOM树中(AppendAllChildren)
- 为DOM节点设置属性(FinalizelnitialChildren)
- 创建好的DOM节点会被赋值给workInProgress节点的stateNode属性
- 将DOM节点插入到DOM树的操作是通过appendAllChildren函数来完成的
- 实际上是将子Fiber节点所对应的DOM节点,挂载到其父Fiber节点所对应的DOM节点里去
当某个 Fiber 节点执行完completeWork,如果其存在兄弟 Fiber 节点(即fiber.sibling !== null),会进入其兄弟 Fiber 的”递”阶段。
如果不存在兄弟 Fiber,会进入父级 Fiber 的”归”阶段。
“递”和”归”阶段会交错执行直到”归”到 rootFiber。至此,协调阶段的工作就结束了。
completeUnitOfWork 开启收集EffectList的大循环,
- 针对当前节点,调用completeWork
- 将当前节点的副作用链插入到其父节点对应的副作用链中
- 以当前节点为起点,循环遍历其兄弟节点及其父节点
effectList是 记录更新的一个单向链表
每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。
effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。
commit阶段(实现更新)
- before mutation阶段,这个阶段DOM节点还没有被渲染到界面.上去
- mutation,这个阶段负责DOM节点的渲染
- layout,这个阶段处理DOM渲染完毕之后的收尾逻辑,它还会把fiberRoot的current指针指向workInProgress Fiber树
Concurrent模式
双缓冲模式
current树与workInProgress树可以对标“双缓冲”模式下的两套缓冲数据:
当current树呈现在用户眼前时,所有的更新都会由workInProgress树来承接workInProgress树将会在用户看不到的地方(内存里)悄悄地完成所有改变
update创建
workLoopSync
workLoopConcurrent
当shouldYield()调用返回为true,就说明当前需要对主线程进行让出了
此时whille循环的判断条件整体为false,while循环将不再继续
任务优先级调度
- startTime:任务的开始时间
- lane: lane越小,任务的优先级就越高
- timerQueue:一个以startTime为排序依据的小顶堆
- 它存储的是startTime大于当前时间(也就是待执行)的任务
- taskQueue: 一个以expirationTime为排序依据的小顶堆
- 它存储的是startTime小于当前时间(也就是已过期)的任务
requestIdleCallback回调的执行的前提条件是当前浏览器处于空闲状态。
即requestIdleCallback的作用是在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务。首先 React 中任务切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React 未完成的任务,是一种合作式调度。
Scheduler(时间切片 优先级)
时间切片是模仿requestIdleCallback
将浏览器在一帧中,执行完重排和重绘后剩下的时间执行js。
为什么不用setTimeout 原因是 setTimeout执行会多消耗大约4ms的时间。
requestIdleCallback这个API只有chrome有。所以react模仿了这个API。
它使用的是MessageChannel。
requestHostCallback = function(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};