React 的源码与原理解读(二):Fiber 与 Fiber 链表树

news/2024/7/15 19:20:05 标签: react.js, 链表, javascript, 数据结构, 前端

写在专栏开头(叠甲)

  1. 作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。

  2. 本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。

  3. 本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。

本一节的内容

本节的内容我们将讲述React中的一个很重要的数据结构——Fiber ,本节先着重说明什么是 Fiber 结构,它的数据结构是什么,以及 React 为什么要在 16.X 版本后引入 Fiber 结构,之后的章节会讲述从 React Element 到 Fiber 树的过程以及 Fiber 树的生成和更新

Fiber 结构

Fiber 是 React 16.x 新增的一个数据结构,它由对应的 React Element 生成,它的作用我们会在后面讲到,我们先来看它的定义。它在源码的这个位置:https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js

javascript">export type Fiber = {
  tag: WorkTag,
  key: null | string,
  elementType: any,
  type: any,
  stateNode: any,
  return: Fiber | null,
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,
  ref:
    | null
    | (((handle: mixed) => void) & {_stringRef: ?string, ...})
    | RefObject,
  refCleanup: null | (() => void),
  pendingProps: any, 
  memoizedProps: any, 
  updateQueue: mixed,
  memoizedState: any,
  dependencies: Dependencies | null,
  mode: TypeOfMode,
  flags: Flags,
  subtreeFlags: Flags,
  deletions: Array<Fiber> | null,
  nextEffect: Fiber | null,
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,
  lanes: Lanes,
  childLanes: Lanes,
  alternate: Fiber | null,
  actualDuration?: number,
  actualStartTime?: number,
  selfBaseDuration?: number,
  treeBaseDuration?: number,
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
  _debugNeedsRemount?: boolean,
  _debugHookTypes?: Array<HookType> | null,
};

它的数据结构实在过于庞大了,我们拆分成及部分来理解它我们只介绍一些重点的属性,其他的属性想要了解的可以查看源码的注释信息或者查阅其他的资料:

首先是和 DOM相关的属性,它和对应的 React Element 息息相关

javascript">tag: WorkTag,        //节点的类型
key: null | string,  //React Element 的 Key 
elementType: any,    //React Element 的 type ,原生标签,类组件或者函数组件
type: any,           //类组件或者函数组件
stateNode: any,      //stateNode 用于记录当前 fiber 所对应的真实 dom 节点或者当前虚拟组件的实例,用于实现对于对DOM的追踪

之后是和整个结构相关的属性,在 React 的 Fiber 架构中,所有的 Fiber 元素将被串联在一起形成一颗双向的链表树,由以下的几个属性来实现

javascript">return: Fiber | null,   //表述一个元素的父亲
child: Fiber | null,    //表述一个元素的第一个孩子
sibling: Fiber | null,  //表述一个元素的兄弟
index: number,          //在兄弟节点中的位置

可以用一张图形象的概括它:
请添加图片描述

之后是用于计算 state 和 props 的部分,这是一个 React 组件很重要的组成部分,不过这篇教程主要以讲解 Fiber 结构为主要目的,所以这部分我们会在后续进行讲解,这里按下不表:

javascript">pendingProps: any,       // 本次渲染需要使用的 props
memoizedProps: any,      // 缓存的上次渲染使用的 props
updateQueue: mixed,      // 用于状态更新、回调函数、DOM更新的队列
memoizedState: any,      /// 缓存的上次渲染后的 state 
dependencies: Dependencies | null,     // contexts、events 等依赖

之后是副作用相关的部分,也就是说,当我们修改了节点的一些属性,比如 state 或者 props 等的时候,我们的 DOM 可能会发生变化,同时这种变化可能还会影响到孩子节点,具体的流程将会在对 React diff 算法的讲解的时候再次深入,这里我们只是先介绍一下相关的逻辑:

在 render 阶段时,react 会采用深度优先遍历,对 fiber 树进行遍历,Fiber 中会用 flags 表示对当前元素的处理,比如是更新或者删除等等,具体可以查看源码的 Flags 枚举,同时 subtreeFlags 用于表示对孩子节点的处理,而 deletions 则表示我们需要删除的子 Fiber 的序列:

javascript">flags: Flags,                       //对当前节点的处理
subtreeFlags: Flags,                //对孩子节点的处理
deletions: Array<Fiber> | null,     //要删除的子节点列表

同时,这个阶段会把每一个有副作用的 fiber 筛选出来,放在一个链表中,以下的三个属性就是来标识这个链表的,这个链表将会在之后的阶段用于更新我们的 DOM

javascript">nextEffect: Fiber | null,     //副作用的下一个 Fiber
firstEffect: Fiber | null,    //第一个有副作用的 Fiber
lastEffect: Fiber | null,     //最后一个有副作用的 Fiber

lanes 是用于表示执行 fiber 任务的优先级的,这个将会在后续的文章中详细的讲解

alternate 是用于双缓冲树这个结构,简单来说就是:

  • react 根据双缓冲机制维护了两个fiber树,一颗用于渲染页面 (current),一颗是 workInProgress Fiber 树,用于在内存中构建,然后方便在构建完成后直接昔换 current Fiber树
  • workInprogress Fiber 树的 alternate 指向 Current Fiber树的对应节点, current 表示页面正在使用的 fiber 树
  • 当 workInprogress Fiber 树构建完成,workInProgress Fiber 则成为了 current 渲染到页面上,而之前的 current 则缓存起来成为下一次的workInProgress Fiber,完成双缓冲模型

双缓存模型的优势就是提升效率,可以防止只用一颗树更新状态的丢失的情况,又加快了 dom 节点的替换与更新,后续我们还会详细聊聊这个结构。

为什么要使用 Fiber

在 React 15.x 版本以及之前的版本,Reconciliation 算法采用了栈调和器,它最大的缺点是:当我们开始一个任务的时候,一切都是同步进行的,一旦开始执行就不会中断,直到所有的工作流程全部结束为止。当一个页面比较复杂的时候,状态变更时,组件树层层递归开始更新,js 主线程就不得不停止其他工作开始进行渲染的逻辑,用户的点击输入等交互事件、页面动画等都不会得到响应,体验就会非常的差。下面是官方的一副漫画来讲解这个过程:函数堆栈的调用就像下图一样,层级很深
请添加图片描述

而从刚刚的数据结构中,我们看到,React 将每个节点都封装到了一个 Fiber 中,整个 DOM 树的渲染任务被分成了一个一个小片。当需要进行渲染的时候,React 会从根节点开始一个一个去更新每一个 Fiber ,每当处理完一个 Fiber ,在处理下一个 Fiber 之前,js 可以转而去处理优先级更高、更需要快速响应的任务,而这个优先级被放置在 lanes 这一位中。

React 可以通过优先级来判定是不是中断 Fiber 的处理,调度所有任务的执行,在这样的架构下,虽然任务总的处理时间不变,但是一些需要快速响应的操作可以得到抢占式的响应,类似于操作系统中对线程的抢占式调度,非常强大。对于用户来说,就不会出现因为页面非常复杂,导致渲染任务耗时很长而一致得不到响应的卡顿感受了,官方把它用如下的漫画来表述:

波谷表示执行分片任务,波峰表示执行其他高优先级任务,分片任务在执行结束后释放 js 主线程,高优先级任务可以抢占它,然后继续执行分片任务

请添加图片描述

总结

这节主要讲述了 React 另一个很重要的数据结构 Fiber 结构,我们现在知道了,一段 Jsx 在 React 里会经过这样的流程:首先被解析成 React Element,再从 React Element 被封装为一个 Fiber ,Fiber 由节点之间的层级关系生成一棵 Fiber 的链表树。

而使用 Fiber 的最重要的原因是,通过 Fiber 可以把一个任务分片,因为 js 的单线程的,如果不分片,任务将会长时间占用 js 主线程,导致用户的请求长时间得不到响应,体验极差。通过 Fiber,高优先级的任务可以在一个分片执行完后抢占 js 主线程,从而提升用户的体验。

那么通过 Fiber 的数据结构,我们得出了一些新的问题:

  1. React 的任务是怎么样调度的 —— 这里会用到我们提到过的 lanes
  2. Fiber 树是怎么样生成和更新的 —— 这里会用到我们提到过的双缓冲树和 alternate
  3. React Element 到 Fiber 的过程发生了什么 —— 这里会处理我们提到过的 state 和 props 的部分

那么在之后的章节里,我们将依次来解决这些问题


http://www.niftyadmin.cn/n/167973.html

相关文章

【1012. 至少有 1 位重复的数字】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给定正整数 n&#xff0c;返回在 [1, n] 范围内具有 至少 1 位 重复数字的正整数的个数。 示例 1&#xff1a; 输入&#xff1a;n 20 输出&#xff1a;1 解释&#xff1a;具有至少 1 位重复数字的…

proxy详细介绍与使用

proxy详细介绍与使用 proxy 对象用于创建一个对象的代理&#xff0c;是在目标对象之前架设一个拦截&#xff0c;外界对该对象的访问&#xff0c;都必须先通过这个拦截。通过这种机制&#xff0c;就可以对外界的访问进行过滤和改写。 ES6 原生提供 Proxy 构造函数&#xff0c;…

仓库管理用什么软件?哪些仓库管理软件适合中小商户?

对于做实体生意的中小商户来说&#xff0c;仓库管理工作是重中之重的&#xff0c;仓库管理的好坏&#xff0c;直接影响着门店销售和财务状况。但对于很多中小商户来说&#xff0c;没有足够的人力和精力去高效地做好仓库管理工作&#xff0c;而借助仓库管理软件或进销存软件来管…

分布式系统有哪些

目录 分布式存储系统 分布式计算系统 分布式消息队列系统 分布式机器学习系统

面向服务架构设计及其应用

面向服务架构&#xff08;Service-Oriented Architecture&#xff0c;SOA&#xff09;是一种软件架构风格&#xff0c;它将软件系统分解为多个服务&#xff0c;每个服务都是一个独立的、可重用的功能单元。这些服务可以通过网络进行通信&#xff0c;协同完成复杂的业务流程。SO…

【Java】对象的构造和初始化

对象的构造和初始化如何初始化对象构造方法概念特性默认初始化就地初始化如何初始化对象 在Java方法内部定义一个局部变量时&#xff0c;必须要初始化&#xff0c;否则会编译失败。 要让上述代码通过编译&#xff0c;非常简单&#xff0c;只需在正式使用a之前&#xff0c;给a设…

Linux调用BufferedImage.createGraphics()卡住不动

问题描述 应用系统在使用上述方法生成验证码图片时一直正常&#xff0c;直到部署在某一台 Linux 机器上一直无法获取到验证码&#xff0c;也没有报错信息。 最终定位到问题&#xff1a;执行到 bufferedimage.createGraphics(); 便停止不动了。 解决方案 在启动脚本中添加 …

Java Object类

介绍 Java是一种面向对象的编程语言&#xff0c;它提供了一个非常强大的类库&#xff0c;其中一个基本类是Object类。Object类是Java类层次结构的根&#xff0c;也是所有Java类的父类。这个类提供了一些通用方法&#xff0c;可以在任何Java对象中使用&#xff0c;这些方法可用…