React Fiber与React v16工作流程浅析

Fiber

它是 React 中的一个基本工作单元,React的一切操作都要基于它去实现。
数据结构如下:

interface Fiber {
  // 标记 Fiber 的实例的类型, 例如函数组件、类组件、宿主组件(即dom)
  tag: WorkTag,
  // class、function组件的构造函数,或者dom组件的标签名。
  type: any,
  // DOM节点 | Class实例
  // 函数组件则为空
  elementType: any,
  stateNode: any,
	
	key: key,
	ref: ref,
	
  // 2. Fiber 结构信息
  // 指向父fiber 节点
  return: Fiber | null,
  // 指向子fiber节点
  child: Fiber | null,
  // 指向兄弟fiber节点
  sibling: Fiber | null,
  // 指向另外一颗树中对应的fiber节点(下文有解释)
复制代码

通俗易懂的说,所有的element都是一个独立的fiber,element的同级元素用sibling链接,子元素用child链接,这样就由上至下形成了一个fiber tree。

工作流程

react的工作流程实际上就是遍历fiber tree,对每个fiber去执行对应的工作。

function workLoopSync() {
  // currentWorkFiber 是一个全局变量,保存当前所要执行的fiber节点,它会从root节点开始
  while (currentWorkFiber !== null) {
    fiberWorkFn(currentWorkFiber);
  }
}
复制代码

fiberWorkFn中会执行当前fiber,然后把这个fiber的child子节点赋值给currentWorkFiber,当子节点不存在时,就把sibling兄弟节点赋值给currentWorkFiber 。

上层的workLoopSync函数的 while循环会根据下个currentWorkFiber去遍历。这样就能实现一个深度优先遍历,从而把所有的fiber执行完毕。

在fiberWorkFn里,又分为两个阶段,一个是beginWork,一个是completeWork。

(beginWork由performUnitOfWork函数控制执行,该函数查看是否有子节点,有则return继续beginWork,无子节点执行completeUnitOfWork函数控制执行completeWork,然后查看有无兄弟节点,有就继续beginWork兄弟节点,否则回溯,completeWork父节点,继续查看有无兄弟节点……)

function performUnitOfWork(workInProgress) {
  const nextChild = beginWork(workInProgress)
  if (nextChild) return nextChild

  // 没有 nextChild, 我们看看这个节点有没有 sibling
  let current = workInProgress
  completeUnitOfWork(current)
  }

function completeUnitOfWork(current){
    while (current) {
    //收集当前节点的effect,然后向上传递
    completeWork(current)
    if (current.sibling) return current.sibling
    //没有 sibling,回到这个节点的父亲,看看有没有sibling
    current = current.return
}
复制代码

协调阶段

beginWork

执行组件render,获取返回的jsx

1.在class组件,会执行实例化,处理state,调用挂载前生命周期钩子等等。最后执行render,获取返回的jsx。

2.在function组件,会执行组件的构造函数,里面包括了hooks的一系列调用,最后获取返回的jsx。

对返回的jsx执行reconcile(也就是俗称的diff),记录差异

1.根据diff生成当前fiber的子节点,并标记上对应的flag(更新、删除、移动)。

2.这个生成的子节点,会返回出去,赋值给currentWorkFiber,然后上层函数workLoopSync进行下一轮遍历,执行这个新生成的fiber节点。

completeWork

当遍历到叶子节点,会执行completeWork,对fiber tree进行一个回溯,回到这个叶子节点的父节点,在发现有sibling兄弟节点时,会将兄弟节点重新赋值给currentWorkFiber,以便上层workLoopSync函数遍历。

1.生成dom节点,并把子孙dom节点插入进去。组成一个虚拟dom树

2.处理props

3.把所有含有副作用的fiber节点用firstEffect和lastEffect链接起来,组成一个链表,以便在commit时去遍历执行。

提交阶段

在completeWork执行到root根节点时,证明所有的工作已经完成,就会执行commitRoot,它又分为三个阶段:
将上一个阶段计算出来的需要处理的副作用(Effects)(包括需要操作的dom更新和需要调用的生命周期钩子)一次性执行了。这个阶段必须同步执行(更准确应为调度),不能被打断。

1.before mutation(执行dom操作前)

调用挂载前的生命周期钩子,比如getSnapshotBeforeUpdate,调度useEffect。

2.mutation(执行dom操作)

执行dom操作,如果有组件被删除,那么还会调用被删除组件的componentWilUnmount或useLayoutEffect的销毁函数

3.layout(执行dom操作后)

切换fiber tree(将workFiberTree替换更新前的currentRenderTree,也就是新节点树替换旧树)
调用componentDidUpdate | componentDidMount或者useLayoutEffect的回调函数。
layout结束后,执行之前调度的useEffect的创建和销毁函数。

1.————在标记为 Snapshot 副作用的节点上调用 getSnapshotBeforeUpdate 生命周期

2.————在标记为 Deletion 副作用的节点上调用 componentWillUnmount 生命周期

3.————执行所有 DOM 插入、更新、删除操作

4.————切换渲染树

5.————在标记为 Placement 副作用的节点上调用 componentDidMount 生命周期

6.————在标记为 Update 副作用的节点上调用 componentDidUpdate 生命周期

总结上文,fiberWorkFn——协调阶段,在beginWork(深度优先遍历、diff记录差异)和completeWork去交替执行每个fiber,在commitRoot时,我们称之为提交阶段

双缓冲

在react中,有两棵fiber tree,一个是current fiber,一个是work(workInProcess) fiber。这两个fiber通过alternate属性来进行联系。

在react中,fiber的根节点叫做 rootFiber(并非这两个fiber tree的根节点,而是所有fiber的根节点)。

current fiber是已经渲染在界面上的fiber。current fiber中的根节点 roorFiber.current = current fiber。

work fiber(workInProgress)是由此次更新,而正在内存中构建的fiber。构建完成后,roorFiber.current = work fiber,就切换为了current fiber,从而渲染到界面上。

由于是在内存中构建,所以它可以随时中断和恢复,不阻塞浏览器渲染。根据优先级而选择先后执行的任务,优先级高的先执行,优先级低的后执行。

export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
module.exports = {
    synchronous,//0,synchronous首屏(首次渲染)用,要求尽量快,不管会不会阻塞UI线程
    task,//1,在next tick之前执行
    animation,//2,animation通过requestAnimationFrame来调度,这样在下一帧就能立即开始动画过程                                                                                                                                                                               
    high,//3,在不久的将来立即执行
    low,//4,稍微延迟执行也没关系
    offscreen,//5,下一次render时或scroll时才执行


复制代码

如果是mount时(第一次挂载),那么没有current fiber,会直接创建

update时一直向下遍历和clone,就会创建出一个新的fiber tree,也就是work fiber tree。然后根据这个新生成的树去提交(执行完毕),渲染到界面上

react的执行是遍历整个fiber tree,在遍历中,会根据current fiber而去clone一个新的work fiber存在cur fiber中的alternate属性中,然后根据协调阶段的beginWork标记的差异(diff)将变化更新到alternate属性中,current fiber的alternate属性中更新好的work fiber的alternate属性又指向current fiber

也就是 cur fiber.alternate => work fiber & work fiber.alternate => cur fiber

当所有的更新执行完成后,将rootFiber.current = work fiber ,并渲染到界面上(此时work fiber已经是 cur fiber了),之前的cur fiber交给浏览器回收。

requestIdelCallback

window.requestIdleCallback(callback[, options])

callback:回调,即空闲时需要执行的任务,该回调函数接收一个IdleDeadline对象作为入参。

其中IdleDeadline对象包含:

didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用。
timeRemaining(),表示当前帧剩余的时间,也可理解为留给任务的时间还有多少。

options:目前 options 只有一个参数

timeout。表示超过这个时间后,如果任务还没执行,则强制执行,不必等待空闲。

客户端线程执行任务时会以帧的形式划分,大部分设备控制在30-60帧是不会影响用户体验;在两个执行帧之间,主线程通常会有一小段空闲时间,requestIdleCallback可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务。

优先级任务由requestIdleCallback处理;

优先级任务,如动画相关的由requestAnimationFrame处理;

requestIdleCallback可以在多个空闲期调用空闲期回调,执行任务;

requestIdleCallback方法提供deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞UI渲染而导致掉帧;

setState先把此次更新放到更新队列 updateQueue 里面,然后调用调度器开始做更新任务。performWork 先调用 workLoop 对 fiber 树进行遍历比较,就是我们上面提到的遍历过程。当此次时间片时间不够遍历完整个 fiber 树,或者遍历并比较完之后workLoop 函数结束。接下来我们判断下 fiber 树是否遍历完或者更新队列 updateQueue 是否还有待更新的任务。如果有则调用 requestIdleCallback 在下个时间片继续干活。nextUnitOfWork 是个全局变量,记录 workLoop 遍历 fiber 树中断在哪个节点。

整体流程

1.第一部分从 用户操作引起setState被调用以后,把接收的 React Element 转换为 Fiber 节点,并为其设置优先级,创建 Update,根据Fiber的优先级加入到Update相应的位置,这部分主要是做一些初始数据的准备。

2.第二部分主要是三个函数:scheduleWork、requestWork、performWork,即调度工作、申请工作、正式工作三部曲,React 16 新增的异步调度的功能则在这部分实现,这部分就是 Schedule 阶段,完成调度主要靠scheduleCallbackWithExpriation这个方法。scheduleCallbackWithExpriation这个方法在不同环境,实现不一样,chrome等览器中使用requestIdleCallback API,没有这个API的浏览器中,通过requestAnimationFrame模拟一个requestIdleCallback,任务调度的过程是:在任务队列中选出高优先级的fiber node执行,调用requestIdleCallback获取所剩时间,若执行时间超过了deathLine,或者突然插入更高优先级的任务,则执行中断,保存当前结果,修改tag标记一下,设置为pending状态,迅速收尾并再调用一个requestIdleCallback,等主线程释放出来再继续。执行到performWorkOnRoot时,第二部分结束。

3.第三部分基本就是 Fiber Reconciler ,分为2个阶段:第一阶段Render/recocilation Phase(协调阶段),遍历所有的 Fiber 节点,通过 Diff 算法计算所有更新工作,产出 EffectList 给到 commit Phase使用,这部分的核心是 beginWork 函数;然后进入Commit Phase(提交阶段),这个阶段不能被打断,不再赘述。

任务调度的过程

在任务队列中选出高优先级的fiber node执行,调用requestIdleCallback获取所剩时间,若执行时间超过了deathLine,或者突然插入更高优先级的任务,则执行中断,保存当前结果,修改fiber node 的tag标记,设置为pending状态,迅速收尾并再调用一个requestIdleCallback,等主线程释放出来再继续恢复任务执行时,检查tag是被中断的任务,会接着继续做任务或者重做。

总结

react的组件架构是由一个个fiber组成的树组成,他的工作流程就是遍历fiber tree去执行每一个工作单元。分为协调阶段(深度遍历并diff产生新树、执行hooks链表并收集effect并链成链表)和提交阶段(处理effect链表,执行完毕切换渲染树)

fiber有新旧两棵树,一个是current fiber,是已经渲染在界面上的。一个是work fiber,由当前的更新触发而在内存中构建的。构建完成,work fiber就会替换cur fiber,然后经过提交阶段完成更新,在dom操作完成后渲染到界面上

一个页面就是一个fiber,这个页面的child就是render函数中的组件或者element,都会有他们自己的sibling,child,return(父级),如果是hook组件会在该fiber中的memoizedState属性保存它自己的hooks链表,在协调阶段通过执行hooks链表得到effect链表。协调阶段时,requestIdleCallback在主线程的空闲期执行低优先级的任务,requestAnimationFrame执行高优先级任务,requestIdleCallback执行完一个fiber的更新后,若下一个任务执行时间超过了deathLine,或者突然插入更高优先级的任务,则执行中断,保存当前结果,修改fiber node 的tag标记,设置为pending状态,恢复任务执行时,检查tag是被中断的任务,会接着继续做任务或者重做。当全部完成时进入提交阶段在提交阶段(不能被打断、同步、遍历)执行effect链表、调度Effect、操作DOM、执行周期函数,完成切换、渲染。

juejin.cn/post/695435…

www.jianshu.com/p/37d7de212…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享