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, null, null, mode);
}
1
如何项目中查看 Fiber 节点
在 chrome 中,将你的 React 项目运行起来,然后打开调试工具,在 Elements
面板中选中一个标签,右键,选择 Store as global variable
然后在 Console
面板中,就会发现有一个叫做 temp1
的全局变量被打印出来。我们执行如下指令查看详细对象信息
console.dir(temp1)
此时我们会发现一个叫做 _reactFiber$xxxxxxx
的字段,展开它,就是当前元素所对应的 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 指向该函数声明本身
type: function 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>
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<any, any> | 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,
deps: Array<mixed> | null,
next: Effect,
};
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