详细解读 Fiber 节点的每一个属性含义

React 知命境第 36 篇,原创第 143 篇

在 React 中,每一个组件都会被转化为对应的 Fiber 节点。这也是我们常说的虚拟 DOM。这篇文章带大家一起来了解一下 Fiber 节点的字段到底都有些什么东西,他们分别代表什么含义。

Fiber 节点在 Reconciler 阶段被创建,它的构造函数如下。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
{
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 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;

  // Effects
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;

  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  this.alternate = null;
}

有了这个构造函数,我们就可以创建各种不同的 Fiber 节点

const createFiber = function(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
): Fiber 
{
  return new FiberNode(tag, pendingProps, key, mode);
};
export function createHostRootFiber(
  tag: RootTag,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
): Fiber 
{
  ...
  return createFiber(HostRoot, nullnull, mode);
}

1

如何项目中查看 Fiber 节点

在 chrome 中,将你的 React 项目运行起来,然后打开调试工具,在 Elements 面板中选中一个标签,右键,选择 Store as global variable

详细解读 Fiber 节点的每一个属性含义

然后在 Console 面板中,就会发现有一个叫做 temp1 的全局变量被打印出来。我们执行如下指令查看详细对象信息

console.dir(temp1)

此时我们会发现一个叫做 _reactFiber$xxxxxxx 的字段,展开它,就是当前元素所对应的 Fiber 节点。如下图所示。此时我们能够看到 Fiber 节点的所有字段具体对应的值,有了这个东西之后,能够更加方便帮助我们学习 Fiber 节点的具体作用。

详细解读 Fiber 节点的每一个属性含义

2

Instance

Fiber 节点的属性值比较多,因此源码中将其进行了分类。其中 Instance 表示构成该节点的基本信息,主要用于判断节点类型。

// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;

tag 是 React 内部自定义的 WorkTag 类型,一共有 25 个值

export type WorkTag =
  | 0
  | 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;

他们所代表的具体含义如下

export const FunctionComponent = 0;
export const ClassComponent = 1;
// Before we know whether it is function or class
export const IndeterminateComponent = 2;
// Root of a host tree. Could be nested inside another node.
export const HostRoot = 3
// A subtree. Could be an entry point to a different renderer.
export const HostPortal = 4;
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;

我们只需要根据语义去理解每个 tag 代表什么类型的组件就可以了,跟使用语法结合起来理解非常简单。

key 为一个唯一值。用于在 diff 阶段能够快速跳过比较,常用于列表组件,普通组件可能没有该值。

type 表示节点类型。HostComponent 表示原生组件类型,此时 type 值为字符串。

type'div'

当组件为函数组件时,type 指向该函数声明本身

typefunction App({}

当组件为 context.Provider 时,type 指向如下

type: {
  $$typeof: Symbol(react.provider),
  _context: {
    $$typeof: Symbole(react.context),
    Consumer: {}
    Provider: {},
    displayName: 'Route',
    ...
  }
}

当节点为根节点时,type 值为 null.

当组件为 memo 包裹的节点时,type 值指向被包裹的组件

function _Child({}

var Child = memo(_Child)
type: _Child,
elementType: {
  $$typeof: Symbol(react.meno),
  compare: null
}

elementType 大多数情况下都跟 type 的值保持一致,只有被 memo 包裹的节点不太一样,具体差别观察上面这个例子。

this.stateNode 指向节点的真实 DOM 对象

2

构建 Fiber tree

// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;

整个页面所对应的 Fiber tree 由多个 Fiber 通过上面的几个引用构建而成。 this.return 指向父节点。this.child 指向子节点。this.sibling 指向下一个兄弟节点。

这里需要注意的是:每个元素的子节点,不是一个数组,而是一个 Fiber 对象。他们共同构成了一个完整的双向链表结构。当父节点有多个子节点时,p.child 只会指向第一个子节点。

例如这个例子,他对应的节点关系如下图

<div>
  <p></p>
  <p></
p>
  <p></p>
</
div>
详细解读 Fiber 节点的每一个属性含义

this.index 表示当前节点是父元素的第几个子节点。

3

存储状态与 hook

  this.ref = null;
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

this.ref 表示当前组件的 ref,原生组件的 ref 指向真实 DOM,自定义组件的 ref 默认值为 null,可以通过 useImpretiveHandle 声明。

this.pendingProps 表示新传入的 props 对象

this.memoizedProps 表示上一次的 props 对象

this.memoizedState 是 hook 链表结构的起点。在 React 中,所有的 hook 被存储在一个链表中。每一个 hook 链表节点的格式如下

export type Hook = {
  memoizedState: any,
  baseState: any,
  baseQueue: Update<anyany> | null,
  queue: any,
  next: Hook | null,
};

不得不说,react 源码里的类型声明写得那真的是一个精彩,any 用得炉火纯青。

对于不同的 hook, hook.memoizedState 所存的值不一样。

例如,对于 useState 而言, hook.memoizedState 表示上一次的 state 值。hook.baseState 表示最新值。hook.next 指向下一个 hook,queue 又是一个新的链表结构,用来存储针对同一个 hook 的多次 setState 操作,它的节点结构如下

export type Update<S, A> = {
  lane: Lane, // 表示优先级
  action: A,
  hasEagerState: boolean,
  eagerState: S | null,
  next: Update<S, A>,
};

this.updateQueue 又是个链表结构,用于收集副作用的操作。该链表结构的节点类型如下

export type Effect = {
  tag: HookFlags,
  create: () => (() => void) | void,
  destroy: (() => void) | void,
  depsArray<mixed> | null,
  nextEffect,
};

this.dependencies 的结构如下,该属性在更新时使用,用于判断是否依赖了 ContextProvider 中的值。

export type Dependencies = {
  lanes: Lanes,
  firstContext: ContextDependency<mixed> | null,
};

this.mode 的值类型为 TypeOfMode,具体类型如下

export type TypeOfMode = number;

export const NoMode = /*                         */ 0b000000;
// TODO: Remove ConcurrentMode by reading from the root tag instead
export const ConcurrentMode = /*                 */ 0b000001;
export const ProfileMode = /*                    */ 0b000010;
export const DebugTracingMode = /*               */ 0b000100;
export const StrictLegacyMode = /*               */ 0b001000;
export const StrictEffectsMode = /*              */ 0b010000;
export const ConcurrentUpdatesByDefaultMode = /* */ 0b100000;

4

更新类型

// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;

this.flags 用来标记节点的更新类型,如果没有更新就是 NoFlags

常见的更新类型如下,更多的更新类型大家可以去源码中 ReactFiberFlags.js 中查看。

export type Flags = number;

// Don't change these two values. They're used by React Dev Tools.
export const NoFlags = /*                      */ 0b00000000000000000000000000;
export const PerformedWork = /*                */ 0b00000000000000000000000001;

// You can change the rest (and add more).
export const Placement = /*                    */ 0b00000000000000000000000010;
export const Update = /*                       */ 0b00000000000000000000000100;
export const Deletion = /*                     */ 0b00000000000000000000001000;
export const ChildDeletion = /*                */ 0b00000000000000000000010000;
export const ContentReset = /*                 */ 0b00000000000000000000100000;
export const Callback = /*                     */ 0b00000000000000000001000000;
export const DidCapture = /*                   */ 0b00000000000000000010000000;
export const ForceClientRender = /*            */ 0b00000000000000000100000000;
export const Ref = /*                          */ 0b00000000000000001000000000;
export const Snapshot = /*                     */ 0b00000000000000010000000000;
export const Passive = /*                      */ 0b00000000000000100000000000;
export const Hydrating = /*                    */ 0b00000000000001000000000000;
export const Visibility = /*                   */ 0b00000000000010000000000000;
export const StoreConsistency = /*             */ 0b00000000000100000000000000;
...

this.deletions 用于存储要被删除的 Fiber 节点。在 diff 过程中,如果发现节点存在但是不能复用,则会把节点存放在这里。

5

优先级

this.lanes = NoLanes;
this.childLanes = NoLanes;

该优先级由 expirationTime 来确定。

6

alternate

React 采用了缓存策略优化更新体验,因此大多数时候,会同时存在两棵 Fiber tree。大家经常听说的 diff 算法也是基于双缓存策略实现。

一棵树表示正在构建过程中的 Fiber Tree,命名为 workInProgress,另一棵树表示已经存在的树,命名为 current

当我们在创建新的 workInProgress 时,会通过 current.alternate 初始化。

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  ...

然后他们在代码中通过 alternate 相互链接。

workInProgress.alternate = current;
current.alternate = workInProgress;

除此之外,这两颗 Fiber Tree 的所有同级节点,也通过 alternate 相互链接,当在 diff 过程中发现可以复用时,可以直接跳过创建过程选择选择 fiber.alternate 以完成复用。

7

总结

在详细了解了 Fiber 对象中每一个字段之后,相信我们在接下来继续了解 React Reconciler 就会容易得多。协调器的整个过程都是围绕 Fiber 展开,这也有利于我们在使用 React 时变得更加合理。

「React 知命境」 是一本从知识体系顶层出发,理论结合实践,通俗易懂,覆盖面广的精品小册,点击下方标签可阅读其他文章。欢迎关注我的公众号,我会持续更新。购买 React 哲学,或者赞赏本文 30 元,可进入 React 付费讨论群,学习氛围良好,学习进度加倍。赞赏之后也能看到 React 哲学的全部内容


原文始发于微信公众号(这波能反杀):详细解读 Fiber 节点的每一个属性含义

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/240192.html

(0)
葫芦侠五楼的头像葫芦侠五楼

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!