介绍React Fiber架构的原理和工作模式

2024-02-27
26709 分钟
...

首先,我们先聊聊React的基本组成:当我们写React组件并使用JSX时,React在底层会将JSX转换为元素的对象结构。例如:

const element = <h1>Hello, world</h1>;

上述代码会被转换为以下形式:

const element = React.createElement(
  'h1',
  null,
  'Hello, world'
);

为了将这个元素渲染到DOM上,React需要创建一种内部实例,用来追踪该组件的所有信息和状态。在早期版本的React中,我们称之为“实例”或“虚拟DOM对象”。但在Fiber架构中,这个新的工作单元就叫做Fiber。

所以,在本质上,Fiber是一个JavaScript对象,代表React的一个工作单元,它包含了与组件相关的信息。一个简化的Fiber对象长这样:

{
  type: 'h1',  
  key: null,   
  props: { ... }, 
  state: { ... }, 
  child: Fiber | null,  
  sibling: Fiber | null,  
  return: Fiber | null,  
  
}

当React开始工作时,它会沿着Fiber树形结构进行,试图完成每个Fiber的工作(例如,比较新旧props,确定是否需要更新组件等)。如果主线程有更重要的工作(例如,响应用户输入),则React可以中断当前工作并返回执行主线程上的任务。

因此,Fiber不仅仅是代表组件的一个内部对象,它还是React的调度和更新机制的核心组成部分。

在React 16之前的版本中,是使用递归的方式处理组件树更新,称为堆栈调和(Stack Reconciliation),这种方法一旦开始就不能中断,直到整个组件树都被遍历完。这种机制在处理大量数据或复杂视图时可能导致主线程被阻塞,从而使应用无法及时响应用户的输入或其他高优先级任务。

Fiber的引入改变了这一情况。Fiber可以理解为是React自定义的一个带有链接关系的DOM树,每个Fiber都代表了一个工作单元,React可以在处理任何Fiber之前判断是否有足够的时间完成该工作,并在必要时中断和恢复工作。

我们来看一下源码里FiberNode的结构:

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

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

  this.ref = null; 
  this.refCleanup = null; 

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

    
  this.mode = mode; 

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

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

  this.alternate = null; 

    
  if (enableProfilerTimer) {
    
  }

    
  if (__DEV__) {
    
  }
}

其实可以理解为是一个更强大的虚拟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节点是否要更新:
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
    if (current !== null) {
        
        const oldProps = current.memoizedProps;
        const newProps = workInProgress.pendingProps;

        if(oldProps !== newProps || hasLegacyContextChanged()) {
            didReceiveUpdate = true; 
        } else {
            
        }
    } else {
        didReceiveUpdate = false; 
    }

    workInProgress.lanes = NoLanes; 

    switch (workInProgress.tag) {
        
        
        case IndeterminateComponent: 
        
        case LazyComponent: 
        
        case FunctionComponent: 
        
        case ClassComponent: 
        

        
        
    }
}
  1. 判断Fiber子节点是更新还是复用:
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any, 
  renderLanes: Lanes,
) {
  if (current === null) {
    
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

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

2、收集副作用列表:completeUnitOfWorkcompleteWork

completeUnitOfWork 负责遍历Fiber节点,同时记录了有副作用节点的关系。下面从源码上理解它的工作:

function completeUnitOfWork(unitOfWork: Fiber): void {
    let completedWork: Fiber = unitOfWork; 
    do {
        const current = completedWork.alternate; 
        const returnFiber = completedWork.return; 

        let next;
        next = completeWork(current, completedWork, renderLanes); 

        if (next !== null) {
          
          workInProgress = next;
          return;
        }
        const siblingFiber = completedWork.sibling;
        if (siblingFiber !== null) {
          
          workInProgress = siblingFiber;
          return;
        }
        
        completedWork = returnFiber;
        workInProgress = completedWork;
    } while (completedWork !== null);

    
  if (workInProgressRootExitStatus === RootInProgress) {
    workInProgressRootExitStatus = RootCompleted;
  } 
}

completeWorkcompleteUnitOfWork 中被调用,下面是 completeWork 的逻辑,主要是根据 tag 进行不同的处理,真正的核心逻辑在 bubbleProperties 里面

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
    switch (workInProgress.tag) {
    
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
         bubbleProperties(workInProgress)
         return null;
    case ClassComponent:
         
         
         bubbleProperties(workInProgress)
         return null;
    case HostComponent:
         
         
         return null;
    
        
  }
}

bubblePropertiescompleteWork 完成了两个工作:

  1. 记录Fiber的副作用标志
  2. 为子Fiber创建链表

这两个工作都从下面这段代码中看出来:

function bubbleProperties(completedWork: Fiber) {
    const didBailout =
    completedWork.alternate !== null &&
    completedWork.alternate.child === completedWork.child; 

    let newChildLanes = NoLanes; 
    let subtreeFlags = NoFlags; 

    if (!didBailout) {
        
        let child = completedWork.child;
        
        while (child !== null) {
          newChildLanes = mergeLanes(
            newChildLanes,
            mergeLanes(child.lanes, child.childLanes),
          );

          subtreeFlags |= child.subtreeFlags;
          subtreeFlags |= child.flags;

          child.return = completedWork; 
          child = child.sibling;
        }
        completedWork.subtreeFlags |= subtreeFlags; 
    } else {
        
        let child = completedWork.child;
        while (child !== null) {
          newChildLanes = mergeLanes(
            newChildLanes,
            mergeLanes(child.lanes, child.childLanes),
          );

          subtreeFlags |= child.subtreeFlags & StaticMask; 
          subtreeFlags |= child.flags & StaticMask; 

          child.return = completedWork;
          child = child.sibling;
        }
        completedWork.subtreeFlags |= subtreeFlags;
    }
    completedWork.childLanes = newChildLanes; 
    return didBailout;
}

调和阶段知识拓展

1、为什么Fiber架构更快?

在上面这段代码里,我们还可以看出来为什么Fiber架构比以前的递归DOM计算要快:flagssubtreeFlags 是16进制的标识,在这里进行按位或(|)运算后,可以记录当前节点本身和子树的副作用类型,通过这个运算结果可以减少节点的遍历,举一个简单的例子说明:

假设有两种标识符
Placement (表示新插入的子节点):0b001
Update (表示子节点已更新):0b010

A
├─ B (Update)
│   └─ D (Placement)
└─ C
   └─ E

这个例子里计算逻辑是这样
1检查到A的flags没有副作用直接复用但subtreeFlags有副作用那么递归检查B和C
2检查到B的flags有复用更新BsubtreeFlags也有副作用则继续检查D
3检查到C的flags没有副作用subtreeFlags也没有副作用那么直接复用C和E
如果节点更多则以此类推
这样的计算方式可以减少递归那些没有副作用的子树或节点所以比以前的版本全部递归的算法要高效

2、调和过程可中断

前面我们提到,调和过程可以被中断,现在我们就看看源码里是怎么进行中断和恢复的。首先,我们要明确可中断的能力是React并发模式(Concurrent Mode)的核心,这种能力使得React可以优先处理高优先级的更新,而推迟低优先级的更新。

可以从下面这段代码理解中断与恢复的处理逻辑:

function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
    
    const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  const prevDispatcher = pushDispatcher(root.containerInfo);
  const prevCacheDispatcher = pushCacheDispatcher();

    if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
        
        
    }

    
    outer: do {
    try {
      if (
        workInProgressSuspendedReason !== NotSuspended &&
        workInProgress !== null
      ) {
        
        const unitOfWork = workInProgress;
        const thrownValue = workInProgressThrownValue;

         
        resumeOrUnwind: switch (workInProgressSuspendedReason) {
          case SuspendedOnError: {
            
            
            break;
          }
          case SuspendedOnData: {
            
            
            break outer;
          }
         case SuspendedOnInstance: {
             
            workInProgressSuspendedReason = SuspendedOnInstanceAndReadyToContinue;
            break outer;
          }
          case SuspendedAndReadyToContinue: {
             
             if (isThenableResolved(thenable)) {
              
              workInProgressSuspendedReason = NotSuspended;
              workInProgressThrownValue = null;
              replaySuspendedUnitOfWork(unitOfWork); 
            } else {
              workInProgressSuspendedReason = NotSuspended;
              workInProgressThrownValue = null;
              throwAndUnwindWorkLoop(unitOfWork, thrownValue); 
            }
            break;
          }
         case SuspendedOnInstanceAndReadyToContinue: {
             
             const isReady = preloadInstance(type, props);
             if (isReady) {
              
              workInProgressSuspendedReason = NotSuspended; 
              workInProgressThrownValue = null;
              const sibling = hostFiber.sibling;
              if (sibling !== null) {
                workInProgress = sibling; 
              } else {
                
                const returnFiber = hostFiber.return;
                if (returnFiber !== null) {
                  workInProgress = returnFiber;
                  completeUnitOfWork(returnFiber); 
                } else {
                  workInProgress = null;
                }
              }
              break resumeOrUnwind;
            }
         }
         
        }
      }

      workLoopConcurrent(); 
      break;
    } catch (thrownValue) {
      handleThrow(root, thrownValue);
    }
  } while (true);

    
  resetContextDependencies();
  popDispatcher(prevDispatcher);
  popCacheDispatcher(prevCacheDispatcher);
  executionContext = prevExecutionContext;

  
  if (workInProgress !== null) {
    
    return RootInProgress; 
  } else {
    
    workInProgressRoot = null; 
    workInProgressRootRenderLanes = NoLanes; 
    finishQueueingConcurrentUpdates(); 
    return workInProgressRootExitStatus; 
  }
}

第二阶段:Commit(提交)

  • 目标: 更新DOM并执行任何副作用。
  • 原理: 遍历在Reconciliation阶段创建的副作用列表进行更新。

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

1、遍历副作用列表:BeforeMutation

export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
): boolean {
  nextEffect = firstChild; 
  commitBeforeMutationEffects_begin(); 

  const shouldFire = shouldFireAfterActiveInstanceBlur; 
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;

  return shouldFire;
}

2、正式提交:CommitMutation

export function commitMutationEffects(
  root: FiberRoot,
  finishedWork: Fiber,
  committedLanes: Lanes,
) {
    
  inProgressLanes = committedLanes;
  inProgressRoot = root;

    
  commitMutationEffectsOnFiber(finishedWork, root, committedLanes);

    
  inProgressLanes = null;
  inProgressRoot = null;
}

3、处理layout effects:commitLayout

export function commitLayoutEffects(
  finishedWork: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
): void {
  inProgressLanes = committedLanes;
  inProgressRoot = root;

  
  const current = finishedWork.alternate;
  
  commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);

  inProgressLanes = null;
  inProgressRoot = null;
}

从源码里我们可以看到,一旦进入提交阶段后,React是无法中断的。

以上内容虽无法覆盖Fiber的方方面面,但可以确保你学完后对Fiber会有一个整体上的认识,并且让你在以后阅读互联网上其它关于Fiber架构的文章时,不再因为基础知识困惑,而是能够根据已有的思路轻松地拓展你大脑里关于Fiber架构的知识网。

如果我的文章对你有用,可以再来看看我的掘金专栏:
1、分享Next.js生态圈技术栈:👉Next.js实战
2、比React官方文档易读且详细的hooks解读:👉精读React hooks

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
react2025-12-02
useEffect与useLayoutEffect对比
在 React Hooks 中, 和 都用于处理副作用逻辑,例如订阅、事件监听、DOM 操作等。但它们有着本质性的执行时机差异,这影响到页面渲染、布局抖动、性能表现等关键点。 1\. 执行时机的核心区别 在 React 的渲染流程中: 1. 渲染(Render phase) React 根据 state/props 计算 UI,生成虚拟 DOM,不访问真实 DOM。 2. 提交(Commit phase) React 将虚拟 DOM 变...
学习
react2025-08-03
在 React 中实现倒计时功能会有什么坑
倒计时 倒计时是一个非常常见的业务场景,但是在 React 中实现起来,却不算简单。 首先我们来看这段倒计时代码,它能否正常执行? 来看看实际表现效果 可以看到计时器在不断执行,但是 的值却没有变。 这是一个很经典的问题:React 闭包陷阱 。 我们来详细分析下: 的依赖数组 是空的,这意味着 effect 只会在组件挂载时执行一次,而不会在 状态更新时重新执行。所以 回调函数中捕获的 值始终是初始值 。 这个问题解决起来也很简单,有...
学习面试
react2025-07-30
React性能优化三剑客:memo、useMemo和useCallback详解
前言 在React开发中,性能优化是一个永恒的话题。今天我们就来深入探讨React提供的三个重要性能优化工具: 、 和 ,它们如何帮助我们构建更高效的React应用。 1\. 为什么需要性能优化? 🤔 React的核心机制是当组件的state或props发生变化时,组件会重新渲染。但有时这种重新渲染是不必要的: 父组件更新导致所有子组件重新渲染 ,即使子组件的props没有变化 复杂计算在每次渲染时重复执行 ,消耗大量资源 函数引用在...
学习面试
react2025-06-05
React 中 useDeferredValue 和 startTransition 的核心区别与使用场景
React 中 和 ,两者都用于优化性能,但适用场景和实现方式不同。 核心概念解析 1\. useDeferredValue:延迟值更新 作用 :告诉 React 延迟更新某个值 ,直到所有高优先级渲染任务完成。 适用场景 :当某个值的更新会触发计算密集型渲染(如复杂列表、图表),但需要优先保证其他高优先级 UI(如输入框)的响应。 机制 : 保持旧值显示,后台计算新值。 计算完成后,用新值更新 UI,期间允许用户继续交互。 示例 :...
学习
react2024-12-18
Antd 样式覆盖
&nbsp; 目前作者所在的业务正在升级 Antd5.0,不得不说 Antd5.0 真的太香了,但是由于团队的 UE 规范进行了大改版,Antd5.0 所有的 必须对齐最新的规范,因此就需要对 Antd5.0 组件做主题定制。 现状 目前针对项目中使用的 antd 高频组件进行了梳理,并且根据规范基于 design token 对这些高频组件进行样式定制,对于主题色、圆角、边框、字体等用户 高的主题都已经满足团队规范。 虽然 Desig...
学习
react2024-12-06
React 19 终于发布,一大波新功能正式升级
这篇文章主要介绍了 React 19 的新功能,包括 Actions 自动处理数据突变相关状态,新增的 useActionState、useFormStatus、useOptimistic 等 hook,新的 use API 读取渲染资源,新的 React DOM 静态 API 生成静态站点,以及 React 服务器组件中的 RSC 和 RSA 等内容。同时提到后续会分享针对旧版功能的优化和改良。 关联问题: React 19 性能如何...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录