老的 React 架构 VS 新的 React 架构
React15 架构
React15 的架构可以分为如下两层:
-
Reconciler(协调器):负责找出组件变化的部分,并协调组件更新的模块。在 React 的架构中,Reconciler负责进行虚拟 DOM 的比对、查找变化、以及通知 Renderer 进行页面渲染等工作。在React15的架构中,Reconciler是架构的核心之一,负责协调和处理组件更新。在系统优化方面,对 Reconciler 的优化可以有效提升React的性能表现。
在 React 中我们可以通过 this.setState、this.forceUpdate、ReactDOM.render 等 API 触发更新。 每当有更新发生时,Reconciler 会做如下工作:
- 调用函数组件、或者 class 组件的 render 方法,将返回的 JSX 转化为虚拟 DOM
- 将虚拟 DOM 和上次更新时的 虚拟 DOM 对比
- 通过对比找出本次更新中变化的虚拟 DOM
- 通知 Renderer 变化的虚拟 DOM 渲染到页面上
-
Renderer(渲染器):负责将变化的组件渲染到页面上的部分。渲染器在 React 应用程序的架构中扮演重要角色,通过将虚拟 DOM 转化为实际页面上的元素,实现内容的显示和更新。在 React15 的架构中,Renderer 接收到 Reconciler(协调器)的通知,负责将变化的组件在当前宿主环境中实时呈现,如 ReactDOM 渲染器用于在浏览器环境中渲染 React 组件。
由于 React 支持跨平台,所以不同平台有不同的 Renderer。前端最熟悉的是负责在浏览器环境渲染的 Renderer -- ReactDOM。每次在更新发生时, Renderer 接收到 Reconciler 通知,将变化的组件渲染在当前宿主环境。
React15 架构的缺点
由于 React15 是基于 Stack Reconcilation(栈调和器)。 它是递归、同步的方式。栈的优点在于用少量的代码就可以实现 diff 功能,并且也非常容易理解,但是也带来了严重的性能问题。
“调和”又译为“协调”,协调的定义,藏在 React 官网对Virtual DOM 及内核这一概念的解释中,原文如下:
Virtual DOM 是一种编程概念。在这个概念里, UI 以一种理想化的,或者说“虚拟的”表现形式被保存于内存中,并通过如 ReactDOM 等类库使之与“真实的” DOM 同步。这一过程叫做协调。
React15 使用的是栈调和器,由于递归执行,所以更新一旦开始,中途就无法中断。当调用层级很深时,递归更新时间超过了屏幕刷新时间间隔,用户交互就会卡顿。
React 16 架构
React16 结构可以分为如下三层:
- Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
- Reconciler(协调器)—— 负责找出变化的组件
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
相比 React15, React16 新增了一个 scheduler(调度器),通过上面的知识我们可以知道 React15 是基于 Stack Reconcilation(栈调和器),当 JS 执行时间过长,带给用户的体验就是所谓的卡顿,那么在 React16 是如何解决这个问题的呢?
答案是:在浏览器的每一帧的时间中,预留一些时间给 JS 线程,React利用这部分时间更新组件,当预留的时间不够用的时候,React 将线程的控制权交还给浏览器使其有时间渲染 UI ,React 则等待下一帧时间的到来,继续被中断的工作。既然,我们以浏览器是否有剩余时间作为任务中断的标准,那么我们就需要一种机制,当浏览器有剩余时间时通知我们。所以 React 就实现了一个 Scheduler(调度器),除了在空闲时触发回调的功能外,它还提供了多种调度优先的任务设置。
Scheduler(调度器)是独立于 React 的库。
Reconciler(协调器)
我们知道 React15 的 Reconciler 是递归处理虚拟 DOM 的,让我们看下 React16 的 Reconciler。
我们可以看见,更新工作从递归变成了可中断的循环过程,每次循环都会调用 shouldYield 来判断当前是否有剩余时间。
同时我们需要注意 React16 中的更新是可中断的, 那么 React 如何解决要是中断了,DOM 渲染不完全的问题呢? 在 React16 中, Reconciler 于 Renderer 不再是严格同步的(就是说,不是以协调完就立刻通知 Renderer 去渲染)。而是当 Scheduler 将任务交给 Reconciler 后,Reconciler 会立刻为变化的虚拟 DOM 打上代表 增、删、更新的标记,类似这样。
全部的标记在这里
整个 Scheduler 与 Reconciler 的工作都在内存中进行。只有当所有组件都完成 Reconciler 的工作,才会统一交给 Renderer。
Renderer(渲染器)
Renderer 根据 Reconciler 为虚拟 DOM 打的标记,同步执行 DOM 操作。
Fiber
在 React15 以前,Reconciler 采用递归的方式创建虚拟 DOM,递归的过程是不能中断的,如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。
为了解决这一问题,React 团队花费了两年时间,重写了 React 核心算法 reconciliation ,在 React16 发布后,为了区分 Reconciler,将之前的 Reconciler 称之为 Stack Reconciler,之后称作 Filer Reconciler(简称 Fiber)。React16 将递归的无法中断的更新重构为异步可中断更新,由于曾经用于递归的虚拟 DOM 数据结构已经无法满足需要,于是全新的 Fiber 架构应运而生。
Fiber 实际上是一种核心算法,为了解决中断树和树庞大的问题,可以认为 Fiber 就是 v16 之后的虚拟 DOM。
Fiber 包含三层含义:
- 最为架构来说,之前 React15 的 Reconciler 采用递归的方式执行,数据保存在递归调用栈中,所以被称为 Stack Reconciler。React16 的 Reconciler 基于 Fiber 节点实现,被称为 Fiber Reconciler;
- 作为静态数据结构来说,每个 Fiber 节点对应一个 React element,保存了该组件的类型(函数组件、class 组件等),对应的 DOM 节点信息;
- 作为动态单元来说,每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除、被插入页面中、被更新...)。
为了更好的理解,我们先来看下 element、fiber、DOM 元素之间的关系:
- element 对象就是我们的 jsx 代码,上面保存了 props 、key、children 等信息;
- DOM 元素就是最终呈现给用户的效果;
- 而 Fiber 就是充当 element 和 DOM 元素的桥梁,简单来说,只要 element 发生改变,就会通过 Fiber 做一次调和,使得对应的 DOM 元素发生改变。
根据源码,可以把 Fiber 节点属性,按三层含义将它们分类来看:
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
// 用于连接其他 Fiber 节点形成 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.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
我们看 FiberNode 这个构造函数里面只是赋值,我嫩再找一下链路上的 Fiber,发现在函数 createFiber 的返回值类型里面出现了 Fiber 类型。
下面我们看一下 Fiber 的代码如下:
export type Fiber = {|
// DOM 节点的相关信息
tag: WorkTag, // 组件类型,用来区分 React 组件类型
key: null | string, // 唯一值
elementType: any, // 元素类型
// 判定 Fiber 节点的类型,用于 diff
type: any,
// 真实 DOM 节点,便于实现 Ref
stateNode: any,
// 链表树
return: Fiber | null, // 父 Fiber
child: Fiber | null, // 第一个子 fiber
sibling: Fiber | null, // 下一个兄弟 fiber
index: number, // 在父 fiber 下面的子 fiber 中的下标
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
// 计算 state 和 props 渲染
pendingProps: any, // 本次渲染要使用的 props
memoizedProps: any, // 上次渲染使用的 props
updateQueue: mixed, // 用于状态更新、回调函数、DOM 更新队列
memoizedState: any, // 上次渲染后 state 状态
dependencies: Dependencies | null, // context、events 等依赖
mode: TypeOfMode,
// Effect
flags: Flags, // 记录更新当时 fiber 的副作用(删除、更新、替换等)状态
subtreeFlags: Flags, // 当前子树的副作用状态
deletions: Array<Fiber> | null, // 要删除的子 fiber
nextEffect: Fiber | null, // 下一个有副作用的 fiber
firstEffect: Fiber | null, // 指向第一个有副作用的 fiber
lastEffect: Fiber | null, // 指向最后一个有副作用的 fiber
// 渲染优先级
lanes: Lanes,
childLanes: Lanes,
alternate: Fiber | null,
actualDuration?: number,
actualStartTime?: number,
selfBaseDuration?: number,
treeBaseDuration?: number,
_debugID?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,
_debugHookTypes?: Array<HookType> | null,
|};
整个 fiber 架构看起来可以分为 DOM 信息、副作用、优先级、链表树等几个模块,下面我们依次拆分解析。
DOM 节点信息
- tag : 我们看到 tag 是 WorkTag 类型,用来区分 React 组件类型.
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;
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
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 FundamentalComponent = 20;
export const ScopeComponent = 21;
export const Block = 22;
export const OffscreenComponent = 23;
export const LegacyHiddenComponent = 24;
上述代码在区分了组件的类型后,在后期协调阶段 beginWork、completeWork 的流程里根据不同的类型组件去做不同的 fiber 节点处理。
Fiber 链表树
作为架构来说,每个 Fiber 节点有个对应的 React element,多个 Fiber 节点是如何连接形成树的呢?Fiber 链表树里面有四个字段: return、child、sibling、index
// 指向父级 Fiber 节点
this.return = null;
// 指向子 Fiber 节点
this.child = null;
// 指向右边第一个兄弟 Fiber 节点
this.sibling = null;
// 父 fiber 下面的子 fiber 下标
this.index = 0;
副作用相关
- flags: Flags,我们可以看到 flags 是 Flags 类型的,用来记录当前节点通过 reconcileChildren 之后的副作用,如插入、删除等操作。
export type Flags = number;
// Don't change these two values. They're used by React Dev Tools.
export const NoFlags = /* */ 0b000000000000000000;
export const PerformedWork = /* */ 0b000000000000000001;
// You can change the rest (and add more).
export const Placement = /* */ 0b000000000000000010;
export const Update = /* */ 0b000000000000000100;
export const PlacementAndUpdate = /* */ 0b000000000000000110;
export const Deletion = /* */ 0b000000000000001000;
export const ContentReset = /* */ 0b000000000000010000;
export const Callback = /* */ 0b000000000000100000;
export const DidCapture = /* */ 0b000000000001000000;
export const Ref = /* */ 0b000000000010000000;
export const Snapshot = /* */ 0b000000000100000000;
export const Passive = /* */ 0b000000001000000000;
// TODO (effects) Remove this bit once the new reconciler is synced to the old.
export const PassiveUnmountPendingDev = /* */ 0b000010000000000000;
export const Hydrating = /* */ 0b000000010000000000;
export const HydratingAndUpdate = /* */ 0b000000010000000100;
// Passive & Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /* */ 0b000000001110100100;
// Union of all host effects
export const HostEffectMask = /* */ 0b000000011111111111;
// These are not really side effects, but we still reuse this field.
export const Incomplete = /* */ 0b000000100000000000;
export const ShouldCapture = /* */ 0b000001000000000000;
export const ForceUpdateForLegacySuspense = /* */ 0b000100000000000000;
// Static tags describe aspects of a fiber that are not specific to a render,
// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
// This enables us to defer more work in the unmount case,
// since we can defer traversing the tree during layout to look for Passive effects,
// and instead rely on the static flag as a signal that there may be cleanup work.
export const PassiveStatic = /* */ 0b001000000000000000;
// Union of side effect groupings as pertains to subtreeFlags
export const BeforeMutationMask = /* */ 0b000000001100001010;
export const MutationMask = /* */ 0b000000010010011110;
export const LayoutMask = /* */ 0b000000000010100100;
export const PassiveMask = /* */ 0b000000001000001000;
// Union of tags that don't get reset on clones.
// This allows certain concepts to persist without recalculting them,
// e.g. whether a subtree contains passive effects or portals.
export const StaticMask = /* */ 0b001000000000000000;
// These flags allow us to traverse to fibers that have effects on mount
// without traversing the entire tree after every commit for
// double invoking
export const MountLayoutDev = /* */ 0b010000000000000000;
export const MountPassiveDev = /* */ 0b100000000000000000;
当然副作用不仅仅是只有一个,所以 React 中在 render 阶段采用的是 深度遍历的策略去找出当前 Fiber 树中所有的副作用,并维护一个副作用 EffectList,与链表相关的字段还有 firstEffect、nextEffect、lastEffect。firstEffect 指向第一个有副作用的 Fiber 节点,lastEffect 指向最后一个具有副作用的 Fiber 节点,中间的都是用 nextEffect 链接,这样组成了一个单向链表。
render 阶段在这里就处理完了,在后面的 commit 阶段,React 会根据 EffectList 里面的 Fiber 节点的副作用,会处理对应的 DOM 节点,然后生成没有副作用的虚拟节点,进行真实 DOM 的创建。
优先级相关
React 作为一个庞大的框架,肯定有自己的一套关于渲染优先级机制,,那么优先级我们就要关注一下 lane 与 alternate,React 中的每个 Fiber 都有自己的 lane(执行优先级),这样在 render 阶段 React 才知道,应该优先把哪个 fiber 交到 commit 阶段去执行。
那这里我们就不得不提到双缓存 Fiber 树了。
在 React 中最多会同时存在两棵 Fiber 树。当前屏幕上展示的内容对应的 Fiber 树称为 current fiber 树,正在内存中构建的 fiber 树称为 workInProgress fiber 树。
current fiber 树中的 fiber 节点被称为 current fiber,workInprogress fiber 树中的 fiber 节点被称为 workInprogress fiber,它们通过 alternate 连接。
而 alternate 在 render 阶段中用来做为指针的,也就是说 React 在状态发生改变的时候,就会根据当前的页面结构,生成两棵 Fiber 树,一棵老树称之为 current fiber,而另外一棵将要生成的树叫做 workInProgress fiber,而 alternate 作为指针,就是把 current fiber 中的每一个节点指向 workInProgress fiber 中的每一个节点。同样的 workInProgress fiber 中指向 current fiber。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
lane 是用 31 位二进制表示唯一值,来进行优先级的判定的,并且位数越低,则优先级越高。
Props & State 相关
- pendingProps: any, 本次渲染需要使用的 props
- memoizedProps: any, 上次使用的 props
- updateQueue: mixed, 用于状态更新、回调函数、DOM 更新的队列
- memoizedState: any, 上次渲染后的 state 状态
- dependencies: Dependencies | null, context、events 等依赖
最后,有关 Fiber 树的创建与更新,我们会在下一篇文章中做详细的介绍。
参考
- [1]React 源码概览
- [2]协调
- [3]Virtual DOM 及内核
- [4]React 源码中的 Fiber 架构
- [5]React 技术揭秘
- [6]React 源码