首先,我们先聊聊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可以细粒度控制节点的行为。

  • 链接属性childsiblingreturn 字段构成了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:memoizedPropspendingPropsmemoizedState 字段让React知道组件的上一个状态和即将应用的状态。通过比较这些值,React可以决定组件是否需要更新,从而避免不必要的渲染,提高性能。

  • 副作用的追踪flagssubtreeFlags 字段标识Fiber及其子树中需要执行的副作用,例如DOM更新、生命周期方法调用等。React会积累这些副作用,然后在Commit阶段一次性执行,从而提高效率。

了解了Fiber的工作原理后,我们可以通过阅读源码来加深对Fiber的理解。React Fiber的工作流程主要分为两个阶段:

第一阶段:Reconciliation(调和)

  • 目标: 确定哪些部分的UI需要更新。
  • 原理: 这是React构建工作进度树的阶段,会比较新的props和旧的Fiber树来确定哪些部分需要更新。

调和阶段又分为三个小阶段:

1、创建与标记更新节点:beginWork

  1. 判断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}
  1. 判断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}

mountChildFibersreconcileChildFibers最终会进入同一个方法createChildReconciler,执行 Fiber 节点的调和(处理诸如新的 Fiber 创建、旧 Fiber 删除或现有 Fiber 更新等操作)。而整个 beginWork 完成后,就会进入 completeWork 流程。

2、收集副作用列表:completeUnitOfWorkcompleteWork

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}

completeWorkcompleteUnitOfWork 中被调用,下面是 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}

bubblePropertiescompleteWork 完成了两个工作:

  1. 记录Fiber的副作用标志
  2. 为子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计算要快:flagssubtreeFlags 是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有复用更新BsubtreeFlags也有副作用则继续检查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阶段创建的副作用列表进行更新。

源码里 commitRootcommitRootImpl 是提交阶段的入口方法,在两个方法中,可以看出来提交阶段也有三个核心小阶段,我们一一讲解:

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

个人笔记记录 2021 ~ 2025