React源码全解
  • React源码学习入门(一)新手如何学习React源码
  • 入门篇
    • React源码学习入门(二)React的render究竟返回的是什么?
    • React源码学习入门(三)React源码codebase架构和调试介绍
    • React源码学习入门(四)深入探究React中的对象池
    • React源码学习入门(五)详解React中的Transaction事务机制
    • React源码学习入门(六)React Component是如何实现的?
    • React源码学习入门(七)详解ReactMount入口
    • React源码学习入门(八)React组件挂载Component细节流程
    • React源码学习入门(九)DOM挂载细节流程
    • React源码学习入门(十)setState是怎么做到异步化的?
    • React源码学习入门(十一)React组件更新流程详解
    • React源码学习入门(十二)DOM组件更新流程与Diff算法
    • React源码学习入门(十三)我用300行代码实现了React
  • 进阶篇
    • React源码学习进阶(一)新版React如何调试源码?
    • React源码学习进阶(二)初识Fiber架构
  • React源码学习进阶(三)rootFiber的创建流程
  • React源码学习进阶(四)render流程的入口逻辑详解
  • React源码学习进阶(五)beginWork如何处理Fiber
  • React源码学习进阶(六)completeWork究竟做了什么
Powered by GitBook
On this page
  • 什么是Fiber
  • Fiber的数据结构
  • 双缓存技术
  • 关于Fiber的学习顺序
  1. 进阶篇

React源码学习进阶(二)初识Fiber架构

PreviousReact源码学习进阶(一)新版React如何调试源码?NextReact源码学习进阶(三)rootFiber的创建流程

Last updated 2 years ago

本文采用React v16.13.1版本源码进行分析

什么是Fiber

我们知道React团队在16版本重写了整个reconciler架构,将之前的stack版本改为了fiber版本,这个过程React团队经历了2年时间,可以说是非常大的一个更新了。

Fiber架构最大的不同是支持了async rendering,后来React团队将这个特性改名为concurrent,在16版本和17版本默认都没有走,在最新的18版本终于成了默认策略。

为什么React团队需要支持这一个特性呢?我想这幅图应该是最清晰能够解释的:

React的渲染,我们可以简单分为几个阶段:

  • Render阶段,这个是之前进行reconcile的阶段,可能也是最耗时的阶段。

  • commit阶段,也就是最终的DOM操作,通过之前的源码分析我们知道它都是在最后统一执行。

  • 浏览器事件响应,实际上由于GUI的渲染线程和JS引擎线程是互斥的,如果前面render阶段占用过长时间,会导致浏览器渲染的卡顿(尤其是动画渲染会有明显的感知),另外事件队列也需要等待JS引擎空闲时才能执行,所以用户的事件也是无法得到响应的。

所以归纳一下React团队实现Fiber架构的最大原因还是以下两点:

  • render时间太长,阻塞界面渲染(尤其是需要帧率的动画渲染)(原因:浏览器GUI线程与JS引擎线程互斥)

  • render时间太长,用户操作无法得到及时响应(原因:浏览器事件循环需要等待JS引擎线程的空闲才能执行)

再仔细剖析下Fiber架构如何解决上述问题:

可以看到一开始render是由事件回调产生的,而在中间会有更高优先级的事件到来,开启了新的render,这就让用户事件、渲染都能得到快速响应。render阶段具体为什么能变成可切分的时间分片技术,后续文章会做深度剖析。

要保证render阶段是可以被打断切分的,一个非常重要的前提就是render过程中不能有副作用,也就是side effects,所以React就在render过程中将所有的side effects进行标记,最后通过一个不可打断的同步commit阶段来执行。

而这套机制也导致了React生命周期的变化:

Fiber的数据结构

Fiber不仅仅代表React架构,它还代表着React底层的数据结构。还记得我们之前分析过程中的Virtual DOM吗,之前是由每个JSX Element组成整个树,而在Fiber重构的架构下,可以理解为它是由一个个的Fiber Node组成的一个Fiber Tree。

export type Fiber = {|  // 节点类型  tag: WorkTag,​  // Element key  key: null | string,​  // element.type  elementType: any,​  // resolved type(Function/Class)  type: any,​  // local state  stateNode: any,    // 链表数据结构相关  return: Fiber | null,  child: Fiber | null,  sibling: Fiber | null,  index: number,​  // ref  ref:    | null    | (((handle: mixed) => void) & {_stringRef: ?string, ...})    | RefObject,​  // props,上一次的存储在memoizedProps中  pendingProps: any,   memoizedProps: any, ​  // 与之前的updateQueue类似,新的state在这里  updateQueue: UpdateQueue<any> | null,  // 上一次的state  memoizedState: any,​  dependencies: Dependencies | null,​  // 节点更新模式  mode: TypeOfMode,​  // effect相关,也是一个链表  effectTag: SideEffectTag,  nextEffect: Fiber | null,  firstEffect: Fiber | null,  lastEffect: Fiber | null,​  // 优先级相关,通过expirationTime表示  expirationTime: ExpirationTime,  childExpirationTime: ExpirationTime,​  // 双缓存架构  alternate: Fiber | null,|};

实际上,Fiber这套数据结构,目的就是为了模拟栈,因为在之前的递归架构里面实际上是通过栈的方式去进行reconcile,通过递归栈来获取当前的virtual DOM和上下文,在fiber架构中,则是通过由fiber节点组成的链表结构来模拟这个栈。

让我们来对比一下它和栈的字段相似之处:

上面是一个典型的对战存储空间,可以做一个关联映射:

  • 基本单位:栈是一个函数,而Fiber是一个FiberNode。

  • 输入:栈是函数的入参,而Fiber是Props

  • 本地状态:栈是函数的本地变量,而Fiber是stateNode

  • 输出:栈是函数的返回值,而Fiber是React Element(其中函数存储在type字段上)

  • 下级:栈是函数的嵌套调用,而Fiber是child

  • 上级引用:栈是返回地址,而Fiber是return

于是整个Fiber就形成了一个链表结构:

而我们每次render的过程,则是对上述的Fiber Tree(双向链表)做深度遍历的过程:

  • 前序遍历,执行beginWork,会进行reconcile的过程。

  • 后续遍历,会执行completeWork,同时对兄弟节点展开深度遍历。

双缓存技术

在Fiber更新过程中React使用到了Double Buffering,一般图形引擎就会采用这类技术,将图片绘制到缓冲区,再一次性传递给屏幕。

在React的Fiber实现中,一个Fiber节点挂载了alternate属性,指向了一个拷贝的Fiber节点,在更新过程中,当前渲染的节点称为current,而我们正在执行更新的节点称为WIP(workInProgress),通过对WIP节点的操作,以减少内存分配和垃圾回收。

Dan之前用了一个形象的比喻,可以将WIP想象为从旧的树中fork出来的分支,无论你对这个fork的版本做了什么,它都不会再影响到旧的树,而在真正完成这些操作后,再将WIP给commit回去,替换掉当前的current。

这个机制也让Error处理变的更加的简单,因为我们当前的current还保留着最正确的渲染版本,即使发生了异常,我们还可以继续沿用旧的节点。

关于Fiber的学习顺序

Fiber这套架构,解决了性能问题,同时也为后面的hooks的实现带来了便利。但是这套架构可以说十分复杂,React团队历时2年才将其完成,足见它的复杂性有多高,因此在学习Fiber的时候,建议从以下顺序来入手:

  • 先学习Sync模式

  • 学习Fiber数据结构与Root的渲染

  • Render阶段的beginWork

  • Render阶段的completeWork

  • commit阶段

  • hook的实现

  • 调度优先级算法实现

  • workloop与时间分片

  • concurrent模式与中断

因为React团队对reconcile是一次比较大的重构,所以我们不必上来就一头扎进调度里面,调度的过程十分复杂,涉及较多前置知识和计算,在优先级方面先后从expirationTime换成了Lane的模型,整体来说可以先跳过这部分的实现。

先从sync模式中可以了解到整个Fiber的架构理念,对比和之前的模式的不同,在对render和commit有了足够的了解之后,再去进行concurrent的研究是我认为更好地学习步骤,后续文章也会按照这个顺序来进行写作。

image-20220913212647763
image-20220913223048276
image-20220913223459154
image-20220913224740451
image-20220913205446800
image-20220913213140803