首先,我们先聊聊React的基本组成:当我们写React组件并使用JSX时,React在底层会将JSX转换为元素的对象结构。例如:
1const element = <h1>Hello, world</h1>;
上述代码会被转换为以下形式:
1const element = React.createElement(
2 'h1',
3 null,
4 'Hello, world'
5);
为了将这个元素渲染到DOM上,React需要创建一种内部实例,用来追踪该组件的所有信息和状态。在早期版本的React中,我们称之为“实例”或“虚拟DOM对象”。但在Fiber架构中,这个新的工作单元就叫做Fiber。
所以,在本质上,Fiber是一个JavaScript对象,代表React的一个工作单元,它包含了与组件相关的信息。一个简化的Fiber对象长这样:
1{
2 type: 'h1',
3 key: null,
4 props: { ... },
5 state: { ... },
6 child: Fiber | null,
7 sibling: Fiber | null,
8 return: Fiber | null,
9
10}
当React开始工作时,它会沿着Fiber树形结构进行,试图完成每个Fiber的工作(例如,比较新旧props,确定是否需要更新组件等)。如果主线程有更重要的工作(例如,响应用户输入),则React可以中断当前工作并返回执行主线程上的任务。
因此,Fiber不仅仅是代表组件的一个内部对象,它还是React的调度和更新机制的核心组成部分。
在React 16之前的版本中,是使用递归的方式处理组件树更新,称为堆栈调和(Stack Reconciliation),这种方法一旦开始就不能中断,直到整个组件树都被遍历完。这种机制在处理大量数据或复杂视图时可能导致主线程被阻塞,从而使应用无法及时响应用户的输入或其他高优先级任务。
Fiber的引入改变了这一情况。Fiber可以理解为是React自定义的一个带有链接关系的DOM树,每个Fiber都代表了一个工作单元,React可以在处理任何Fiber之前判断是否有足够的时间完成该工作,并在必要时中断和恢复工作。
我们来看一下源码里FiberNode的结构:
1function FiberNode(
2 this: $FlowFixMe,
3 tag: WorkTag,
4 pendingProps: mixed,
5 key: null | string,
6 mode: TypeOfMode,
7) {
8
9 this.tag = tag;
10 this.key = key;
11 this.elementType = null;
12 this.type = null;
13 this.stateNode = null;
14
15
16 this.return = null;
17 this.child = null;
18 this.sibling = null;
19 this.index = 0;
20
21 this.ref = null;
22 this.refCleanup = null;
23
24
25 this.pendingProps = pendingProps;
26 this.memoizedProps = null;
27 this.updateQueue = null;
28 this.memoizedState = null;
29 this.dependencies = null;
30
31
32 this.mode = mode;
33
34
35 this.flags = NoFlags;
36 this.subtreeFlags = NoFlags;
37 this.deletions = null;
38
39 this.lanes = NoLanes;
40 this.childLanes = NoLanes;
41
42 this.alternate = null;
43
44
45 if (enableProfilerTimer) {
46
47 }
48
49
50 if (__DEV__) {
51
52 }
53}
其实可以理解为是一个更强大的虚拟DOM。
Fiber工作原理中最核心的点就是:可以中断和恢复,这个特性增强了React的并发性和响应性。
实现可中断和恢复的原因就在于:Fiber的数据结构里提供的信息让React可以追踪工作进度、管理调度和同步更新到DOM
现在我们来聊聊Fiber工作原理中的几个关键点:
-
单元工作:每个Fiber节点代表一个单元,所有Fiber节点共同组成一个Fiber链表树(有链接属性,同时又有树的结构),这种结构让React可以细粒度控制节点的行为。
-
链接属性:
child
、sibling
和return
字段构成了Fiber之间的链接关系,使React能够遍历组件树并知道从哪里开始、继续或停止工作。
- 双缓冲技术: React在更新时,会根据现有的Fiber树(Current Tree)创建一个新的临时树(Work-in-progress (WIP) Tree),WIP-Tree包含了当前更新受影响的最高节点直至其所有子孙节点。Current Tree是当前显示在页面上的视图,WIP-Tree则是在后台进行更新,WIP-Tree更新完成后会复制其它节点,并最终替换掉Current Tree,成为新的Current Tree。因为React在更新时总是维护了两个Fiber树,所以可以随时进行比较、中断或恢复等操作,而且这种机制让React能够同时具备拥有优秀的渲染性能和UI的稳定性。
-
State 和 Props:
memoizedProps
、pendingProps
和memoizedState
字段让React知道组件的上一个状态和即将应用的状态。通过比较这些值,React可以决定组件是否需要更新,从而避免不必要的渲染,提高性能。 -
副作用的追踪:
flags
和subtreeFlags
字段标识Fiber及其子树中需要执行的副作用,例如DOM更新、生命周期方法调用等。React会积累这些副作用,然后在Commit阶段一次性执行,从而提高效率。
了解了Fiber的工作原理后,我们可以通过阅读源码来加深对Fiber的理解。React Fiber的工作流程主要分为两个阶段:
第一阶段:Reconciliation(调和)
- 目标: 确定哪些部分的UI需要更新。
- 原理: 这是React构建工作进度树的阶段,会比较新的props和旧的Fiber树来确定哪些部分需要更新。
调和阶段又分为三个小阶段:
1、创建与标记更新节点:beginWork
- 判断Fiber节点是否要更新:
1function beginWork(
2 current: Fiber | null,
3 workInProgress: Fiber,
4 renderLanes: Lanes,
5): Fiber | null {
6 if (current !== null) {
7
8 const oldProps = current.memoizedProps;
9 const newProps = workInProgress.pendingProps;
10
11 if(oldProps !== newProps || hasLegacyContextChanged()) {
12 didReceiveUpdate = true;
13 } else {
14
15 }
16 } else {
17 didReceiveUpdate = false;
18 }
19
20 workInProgress.lanes = NoLanes;
21
22 switch (workInProgress.tag) {
23
24
25 case IndeterminateComponent:
26
27 case LazyComponent:
28
29 case FunctionComponent:
30
31 case ClassComponent:
32
33
34
35
36 }
37}
- 判断Fiber子节点是更新还是复用:
1export function reconcileChildren(
2 current: Fiber | null,
3 workInProgress: Fiber,
4 nextChildren: any,
5 renderLanes: Lanes,
6) {
7 if (current === null) {
8
9 workInProgress.child = mountChildFibers(
10 workInProgress,
11 null,
12 nextChildren,
13 renderLanes,
14 );
15 } else {
16
17 workInProgress.child = reconcileChildFibers(
18 workInProgress,
19 current.child,
20 nextChildren,
21 renderLanes,
22 );
23 }
24}
mountChildFibers
和reconcileChildFibers
最终会进入同一个方法createChildReconciler
,执行 Fiber 节点的调和(处理诸如新的 Fiber 创建、旧 Fiber 删除或现有 Fiber 更新等操作)。而整个 beginWork
完成后,就会进入 completeWork
流程。
2、收集副作用列表:completeUnitOfWork
和completeWork
completeUnitOfWork
负责遍历Fiber节点,同时记录了有副作用节点的关系。下面从源码上理解它的工作:
1function completeUnitOfWork(unitOfWork: Fiber): void {
2 let completedWork: Fiber = unitOfWork;
3 do {
4 const current = completedWork.alternate;
5 const returnFiber = completedWork.return;
6
7 let next;
8 next = completeWork(current, completedWork, renderLanes);
9
10 if (next !== null) {
11
12 workInProgress = next;
13 return;
14 }
15 const siblingFiber = completedWork.sibling;
16 if (siblingFiber !== null) {
17
18 workInProgress = siblingFiber;
19 return;
20 }
21
22 completedWork = returnFiber;
23 workInProgress = completedWork;
24 } while (completedWork !== null);
25
26
27 if (workInProgressRootExitStatus === RootInProgress) {
28 workInProgressRootExitStatus = RootCompleted;
29 }
30}
completeWork
在 completeUnitOfWork
中被调用,下面是 completeWork
的逻辑,主要是根据 tag 进行不同的处理,真正的核心逻辑在 bubbleProperties
里面
1function completeWork(
2 current: Fiber | null,
3 workInProgress: Fiber,
4 renderLanes: Lanes,
5): Fiber | null {
6 const newProps = workInProgress.pendingProps;
7 switch (workInProgress.tag) {
8
9 case FunctionComponent:
10 case ForwardRef:
11 case SimpleMemoComponent:
12 bubbleProperties(workInProgress)
13 return null;
14 case ClassComponent:
15
16
17 bubbleProperties(workInProgress)
18 return null;
19 case HostComponent:
20
21
22 return null;
23
24
25 }
26}
bubbleProperties
为 completeWork
完成了两个工作:
- 记录Fiber的副作用标志
- 为子Fiber创建链表
这两个工作都从下面这段代码中看出来:
1function bubbleProperties(completedWork: Fiber) {
2 const didBailout =
3 completedWork.alternate !== null &&
4 completedWork.alternate.child === completedWork.child;
5
6 let newChildLanes = NoLanes;
7 let subtreeFlags = NoFlags;
8
9 if (!didBailout) {
10
11 let child = completedWork.child;
12
13 while (child !== null) {
14 newChildLanes = mergeLanes(
15 newChildLanes,
16 mergeLanes(child.lanes, child.childLanes),
17 );
18
19 subtreeFlags |= child.subtreeFlags;
20 subtreeFlags |= child.flags;
21
22 child.return = completedWork;
23 child = child.sibling;
24 }
25 completedWork.subtreeFlags |= subtreeFlags;
26 } else {
27
28 let child = completedWork.child;
29 while (child !== null) {
30 newChildLanes = mergeLanes(
31 newChildLanes,
32 mergeLanes(child.lanes, child.childLanes),
33 );
34
35 subtreeFlags |= child.subtreeFlags & StaticMask;
36 subtreeFlags |= child.flags & StaticMask;
37
38 child.return = completedWork;
39 child = child.sibling;
40 }
41 completedWork.subtreeFlags |= subtreeFlags;
42 }
43 completedWork.childLanes = newChildLanes;
44 return didBailout;
45}
调和阶段知识拓展
1、为什么Fiber架构更快?
在上面这段代码里,我们还可以看出来为什么Fiber架构比以前的递归DOM计算要快:flags
或 subtreeFlags
是16进制的标识,在这里进行按位或(|
)运算后,可以记录当前节点本身和子树的副作用类型,通过这个运算结果可以减少节点的遍历,举一个简单的例子说明:
1假设有两种标识符:
2Placement (表示新插入的子节点):0b001
3Update (表示子节点已更新):0b010
4
5A
6├─ B (Update)
7│ └─ D (Placement)
8└─ C
9 └─ E
10
11这个例子里,计算逻辑是这样:
121、检查到A的flags没有副作用,直接复用,但subtreeFlags有副作用,那么递归检查B和C
132、检查到B的flags有复用,更新B,subtreeFlags也有副作用,则继续检查D
143、检查到C的flags没有副作用,subtreeFlags也没有副作用,那么直接复用C和E
15如果节点更多,则以此类推。
16这样的计算方式可以减少递归那些没有副作用的子树或节点,所以比以前的版本全部递归的算法要高效
2、调和过程可中断
前面我们提到,调和过程可以被中断,现在我们就看看源码里是怎么进行中断和恢复的。首先,我们要明确可中断的能力是React并发模式(Concurrent Mode)的核心,这种能力使得React可以优先处理高优先级的更新,而推迟低优先级的更新。
可以从下面这段代码理解中断与恢复的处理逻辑:
1function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
2
3 const prevExecutionContext = executionContext;
4 executionContext |= RenderContext;
5 const prevDispatcher = pushDispatcher(root.containerInfo);
6 const prevCacheDispatcher = pushCacheDispatcher();
7
8 if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
9
10
11 }
12
13
14 outer: do {
15 try {
16 if (
17 workInProgressSuspendedReason !== NotSuspended &&
18 workInProgress !== null
19 ) {
20
21 const unitOfWork = workInProgress;
22 const thrownValue = workInProgressThrownValue;
23
24
25 resumeOrUnwind: switch (workInProgressSuspendedReason) {
26 case SuspendedOnError: {
27
28
29 break;
30 }
31 case SuspendedOnData: {
32
33
34 break outer;
35 }
36 case SuspendedOnInstance: {
37
38 workInProgressSuspendedReason = SuspendedOnInstanceAndReadyToContinue;
39 break outer;
40 }
41 case SuspendedAndReadyToContinue: {
42
43 if (isThenableResolved(thenable)) {
44
45 workInProgressSuspendedReason = NotSuspended;
46 workInProgressThrownValue = null;
47 replaySuspendedUnitOfWork(unitOfWork);
48 } else {
49 workInProgressSuspendedReason = NotSuspended;
50 workInProgressThrownValue = null;
51 throwAndUnwindWorkLoop(unitOfWork, thrownValue);
52 }
53 break;
54 }
55 case SuspendedOnInstanceAndReadyToContinue: {
56
57 const isReady = preloadInstance(type, props);
58 if (isReady) {
59
60 workInProgressSuspendedReason = NotSuspended;
61 workInProgressThrownValue = null;
62 const sibling = hostFiber.sibling;
63 if (sibling !== null) {
64 workInProgress = sibling;
65 } else {
66
67 const returnFiber = hostFiber.return;
68 if (returnFiber !== null) {
69 workInProgress = returnFiber;
70 completeUnitOfWork(returnFiber);
71 } else {
72 workInProgress = null;
73 }
74 }
75 break resumeOrUnwind;
76 }
77 }
78
79 }
80 }
81
82 workLoopConcurrent();
83 break;
84 } catch (thrownValue) {
85 handleThrow(root, thrownValue);
86 }
87 } while (true);
88
89
90 resetContextDependencies();
91 popDispatcher(prevDispatcher);
92 popCacheDispatcher(prevCacheDispatcher);
93 executionContext = prevExecutionContext;
94
95
96 if (workInProgress !== null) {
97
98 return RootInProgress;
99 } else {
100
101 workInProgressRoot = null;
102 workInProgressRootRenderLanes = NoLanes;
103 finishQueueingConcurrentUpdates();
104 return workInProgressRootExitStatus;
105 }
106}
第二阶段:Commit(提交)
- 目标: 更新DOM并执行任何副作用。
- 原理: 遍历在Reconciliation阶段创建的副作用列表进行更新。
源码里 commitRoot
和 commitRootImpl
是提交阶段的入口方法,在两个方法中,可以看出来提交阶段也有三个核心小阶段,我们一一讲解:
1、遍历副作用列表:BeforeMutation
1export function commitBeforeMutationEffects(
2 root: FiberRoot,
3 firstChild: Fiber,
4): boolean {
5 nextEffect = firstChild;
6 commitBeforeMutationEffects_begin();
7
8 const shouldFire = shouldFireAfterActiveInstanceBlur;
9 shouldFireAfterActiveInstanceBlur = false;
10 focusedInstanceHandle = null;
11
12 return shouldFire;
13}
2、正式提交:CommitMutation
1export function commitMutationEffects(
2 root: FiberRoot,
3 finishedWork: Fiber,
4 committedLanes: Lanes,
5) {
6
7 inProgressLanes = committedLanes;
8 inProgressRoot = root;
9
10
11 commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
12
13
14 inProgressLanes = null;
15 inProgressRoot = null;
16}
3、处理layout effects:commitLayout
1export function commitLayoutEffects(
2 finishedWork: Fiber,
3 root: FiberRoot,
4 committedLanes: Lanes,
5): void {
6 inProgressLanes = committedLanes;
7 inProgressRoot = root;
8
9
10 const current = finishedWork.alternate;
11
12 commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
13
14 inProgressLanes = null;
15 inProgressRoot = null;
16}
从源码里我们可以看到,一旦进入提交阶段后,React是无法中断的。
以上内容虽无法覆盖Fiber的方方面面,但可以确保你学完后对Fiber会有一个整体上的认识,并且让你在以后阅读互联网上其它关于Fiber架构的文章时,不再因为基础知识困惑,而是能够根据已有的思路轻松地拓展你大脑里关于Fiber架构的知识网。
如果我的文章对你有用,可以再来看看我的掘金专栏:
1、分享Next.js生态圈技术栈:👉Next.js实战
2、比React官方文档易读且详细的hooks解读:👉精读React hooks