万字长文介绍React Fiber架构的原理和工作模式

🧑‍💻
推荐全栈学习资源:
  • Next.js 中文文档:样式和官网一样的中文文档,创造沉浸式Next.js中文学习体验。
  • 《Chrome插件全栈开发》:真实出海项目的实战教学课,讲解Chrome插件和Next.js端的全栈开发,帮助你半个月内成为全栈出海工程师。
  • 💡

    为了写这篇文章,我花了5天时间阅读Fiber的核心源码,尽管本文字符数过万,但相对于几十万行Fiber源码来说,只能算是介绍了Fiber的基础知识,所以如果内容有纰漏,请在评论区为我指正,我会进行更新,如果阅读文章后有哪个关于Fiber的专题你想了解,也可以评论区提出来,我很乐意继续研究源码和分享知识。

    自React 16开始,React引入了Fiber架构,解决了以前的更新机制的问题,即在长时间的更新过程中,主线程会被阻塞,导致应用无法及时响应用户输入。本文我们就来聊聊Fiber是什么以及它的底层原理,学习完本文可以让你对Fiber架构的原理有一个比较清晰的认识。

    本文首发于我的博客「👉J实验室

    欢迎加入「🌍独立全栈开发交流群」,一起学习交流前端和Node端技术

    Fiber是什么?

    首先,我们先聊聊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,   // React key
      props: { ... }, // 输入的props
      state: { ... }, // 组件的state (如果是class组件或带有state的function组件)
      child: Fiber | null,  // 第一个子元素的Fiber
      sibling: Fiber | null,  // 下一个兄弟元素的Fiber
      return: Fiber | null,  // 父元素的Fiber
      // ...其他属性
    }

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

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

    为什么需要Fiber?

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

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

    Fiber的结构

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

    function FiberNode(
      this: $FlowFixMe,
      tag: WorkTag,
      pendingProps: mixed,
      key: null | string,
      mode: TypeOfMode,
    ) {
      // 基本属性
      this.tag = tag; // 描述此Fiber的启动模式的值(LegacyRoot = 0; ConcurrentRoot = 1)
      this.key = key; // React key
      this.elementType = null; // 描述React元素的类型。例如,对于JSX<App />,elementType是App
      this.type = null; // 组件类型
      this.stateNode = null; // 对于类组件,这是类的实例;对于DOM元素,它是对应的DOM节点。
     
      // Fiber链接
      this.return = null; // 指向父Fiber
      this.child = null; // 指向第一个子Fiber
      this.sibling = null; // 指向其兄弟Fiber
      this.index = 0; // 子Fiber中的索引位置
     
      this.ref = null; // 如果组件上有ref属性,则该属性指向它
      this.refCleanup = null; // 如果组件上的ref属性在更新中被删除或更改,此字段会用于追踪需要清理的旧ref
     
    	// Props & State
      this.pendingProps = pendingProps; // 正在等待处理的新props
      this.memoizedProps = null; // 上一次渲染时的props
      this.updateQueue = null; // 一个队列,包含了该Fiber上的状态更新和副作用
      this.memoizedState = null; // 上一次渲染时的state
      this.dependencies = null; // 该Fiber订阅的上下文或其他资源的描述
     
    	// 工作模式
      this.mode = mode; // 描述Fiber工作模式的标志(例如Concurrent模式、Blocking模式等)。
     
      // Effects
      this.flags = NoFlags; // 描述该Fiber发生的副作用的标志(十六进制的标识)
      this.subtreeFlags = NoFlags; // 描述该Fiber子树中发生的副作用的标志(十六进制的标识)
      this.deletions = null; // 在commit阶段要删除的子Fiber数组
     
      this.lanes = NoLanes; // 与React的并发模式有关的调度概念。
      this.childLanes = NoLanes; // 与React的并发模式有关的调度概念。
     
      this.alternate = null; // Current Tree和Work-in-progress (WIP) Tree的互相指向对方tree里的对应单元
     
    	// 如果启用了性能分析
      if (enableProfilerTimer) {
    		// ……
      }
     
    	// 开发模式中
      if (__DEV__) {
        // ……
      }
    }

    其实可以理解为是一个更强大的虚拟DOM。

    Fiber工作原理

    Fiber工作原理中最核心的点就是:可以中断和恢复,这个特性增强了React的并发性和响应性。

    实现可中断和恢复的原因就在于:Fiber的数据结构里提供的信息让React可以追踪工作进度、管理调度和同步更新到DOM

    现在我们来聊聊Fiber工作原理中的几个关键点:

    • 单元工作:每个Fiber节点代表一个单元,所有Fiber节点共同组成一个Fiber链表树(有链接属性,同时又有树的结构),这种结构让React可以细粒度控制节点的行为。

    • 链接属性childsiblingreturn 字段构成了Fiber之间的链接关系,使React能够遍历组件树并知道从哪里开始、继续或停止工作。

      1.png

    • 双缓冲技术: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的稳定性。

      2.png

    • State 和 PropsmemoizedPropspendingPropsmemoizedState 字段让React知道组件的上一个状态和即将应用的状态。通过比较这些值,React可以决定组件是否需要更新,从而避免不必要的渲染,提高性能。

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

    Fiber工作流程

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

    第一阶段:Reconciliation(调和)

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

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

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

    1. 判断Fiber节点是否要更新
    // packages/react-reconciler/src/ReactFiberBeginWork.js
    // 以下只是核心逻辑的代码,不是beginWork的完整源码
    function beginWork(
      current: Fiber | null,
      workInProgress: Fiber,
      renderLanes: Lanes,
    ): Fiber | null {
    	if (current !== null) {
    		// 这是旧节点,需要检查props和context是否有变化再确认是否需要更新节点
    		const oldProps = current.memoizedProps;
        const newProps = workInProgress.pendingProps;
     
    		if(oldProps !== newProps || hasLegacyContextChanged()) {
    			didReceiveUpdate = true; // props和context有变化,说明节点有更新
    		} else {
    			// 其它特殊情况的判断
    		}
    	} else {
    		didReceiveUpdate = false; // 这是新节点,要创建,而不是更新
    	}
     
    	workInProgress.lanes = NoLanes; // 进入beginWork表示开始新的工作阶段,所以要把旧的workInProgress优先级清除掉
     
    	switch (workInProgress.tag) {
    		// 通过workInProgress的tag属性来确定如何处理当前的Fiber节点
    		// 每一种tag对应一种不同的Fiber类型,进入不同的调和过程(reconcileChildren())
    		case IndeterminateComponent: // 尚未确定其类型的组件
    		// ……
    		case LazyComponent: // 懒加载组件
    		// ……
    		case FunctionComponent: // 函数组件
    		// ……
    		case ClassComponent: // 类组件
    		// ……
    		
    		// 其它多种Fiber类型
    		// case ……
    	}
    }
    1. 判断Fiber子节点是更新还是复用:
    // packages/react-reconciler/src/ReactFiberBeginWork.js
    export function reconcileChildren(
      current: Fiber | null,
      workInProgress: Fiber,
      nextChildren: any, // 要调和的新的子元素
      renderLanes: Lanes,
    ) {
      if (current === null) {
    		// 如果current为空,说明这个Fiber是首次渲染,React会为nextChildren生成一组新的Fiber节点
        workInProgress.child = mountChildFibers(
          workInProgress,
          null,
          nextChildren,
          renderLanes,
        );
      } else {
    		// 当current非空时,React会利用现有的Fiber节点(current.child)和新的子元素(nextChildren)进行调和
        workInProgress.child = reconcileChildFibers(
          workInProgress,
          current.child,
          nextChildren,
          renderLanes,
        );
      }
    }

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

    2、收集副作用列表:completeUnitOfWorkcompleteWork

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

    // packages/react-reconciler/src/ReactFiberWorkLoop.js
    // 以下只是核心逻辑的代码,不是completeUnitOfWork的完整源码
    function completeUnitOfWork(unitOfWork: Fiber): void {
    	let completedWork: Fiber = unitOfWork; // 当前正在完成的工作单元
    	do {
    		const current = completedWork.alternate; // 当前Fiber节点在另一棵树上的版本
    		const returnFiber = completedWork.return; // 当前Fiber节点的父节点
     
    		let next;
        next = completeWork(current, completedWork, renderLanes); // 调用completeWork函数
     
    		if (next !== null) {
    			// 当前Fiber还有工作要完成
    		  workInProgress = next;
    		  return;
    		}
    		const siblingFiber = completedWork.sibling;
        if (siblingFiber !== null) {
    			// 如果有兄弟节点,则进入兄弟节点的工作
          workInProgress = siblingFiber;
          return;
        }
        // 如果没有兄弟节点,回到父节点继续
        completedWork = returnFiber;
        workInProgress = completedWork;
    	} while (completedWork !== null);
     
    	// 如果处理了整个Fiber树,更新workInProgressRootExitStatus为RootCompleted,表示调和已完成
      if (workInProgressRootExitStatus === RootInProgress) {
        workInProgressRootExitStatus = RootCompleted;
      } 
    }

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

    // packages/react-reconciler/src/ReactFiberCompleteWork.js
    // 以下只是核心逻辑的代码,不是completeWork的完整源码
    function completeWork(
      current: Fiber | null,
      workInProgress: Fiber,
      renderLanes: Lanes,
    ): Fiber | null {
      const newProps = workInProgress.pendingProps;
    	switch (workInProgress.tag) {
        // 多种tag
        case FunctionComponent:
        case ForwardRef:
        case SimpleMemoComponent:
    			 bubbleProperties(workInProgress)
          return null;
        case ClassComponent:
    			 // 省略逻辑
          // ……
    			 bubbleProperties(workInProgress)
          return null;
        case HostComponent:
    			 // 省略逻辑
          // ……
          return null;
        // 多种tag
    		// ……
      }
    }

    bubblePropertiescompleteWork 完成了两个工作:

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

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

    // packages/react-reconciler/src/ReactFiberCompleteWork.js
    // 以下只是核心逻辑的代码,不是bubbleProperties的完整源码
    function bubbleProperties(completedWork: Fiber) {
    	const didBailout =
        completedWork.alternate !== null &&
        completedWork.alternate.child === completedWork.child; // 当前的Fiber与其alternate(备用/上一次的Fiber)有相同的子节点,则跳过更新
     
      let newChildLanes = NoLanes; // 合并后的子Fiber的lanes
      let subtreeFlags = NoFlags; // 子树的flags。
     
    	if (!didBailout) {
    		// 没有bailout,需要冒泡子Fiber的属性到父Fiber
    		let child = completedWork.child;
    		// 遍历子Fiber,并合并它们的lanes和flags
        while (child !== null) {
          newChildLanes = mergeLanes(
            newChildLanes,
            mergeLanes(child.lanes, child.childLanes),
          );
     
          subtreeFlags |= child.subtreeFlags;
          subtreeFlags |= child.flags;
     
          child.return = completedWork; // Fiber的return指向父Fiber,确保整个Fiber树的一致性
          child = child.sibling;
        }
    		completedWork.subtreeFlags |= subtreeFlags; // 合并所有flags(副作用)
    	} else {
    		// 有bailout,只冒泡那些具有“静态”生命周期的flags
    		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; // 获取所有子Fiber的lanes。
     
    	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有复用,更新B,subtreeFlags也有副作用,则继续检查D
    3、检查到C的flags没有副作用,subtreeFlags也没有副作用,那么直接复用C和E
    如果节点更多,则以此类推。
    这样的计算方式可以减少递归那些没有副作用的子树或节点,所以比以前的版本全部递归的算法要高效

    2、调和过程可中断

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

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

    // packages/react-reconciler/src/ReactFiberWorkLoop.js
    // 以下只是核心逻辑的代码,不是renderRootConcurrent的完整源码
    function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
    	// 保存当前的执行上下文和 dispatcher
    	const prevExecutionContext = executionContext;
      executionContext |= RenderContext;
      const prevDispatcher = pushDispatcher(root.containerInfo);
      const prevCacheDispatcher = pushCacheDispatcher();
     
    	if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    		// 如果当前的工作进度树与传入的 root 或 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: {
    						 // 将挂起的原因更新为SuspendedOnInstanceAndReadyToContinue并中断工作循环,标记为稍后准备好继续执行
                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; // 该fiber已完成,不需要再挂起
                  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;
                }
    					 }
    					 // 还有其它case
            }
          }
     
          workLoopConcurrent(); // 如果没有任何工作被挂起,那么就会继续处理工作循环。
          break;
        } catch (thrownValue) {
          handleThrow(root, thrownValue);
        }
      } while (true);
     
    	// 重置了之前保存的执行上下文和dispatcher,确保后续的代码不会受到这个函数的影响
      resetContextDependencies();
    	popDispatcher(prevDispatcher);
      popCacheDispatcher(prevCacheDispatcher);
      executionContext = prevExecutionContext;
     
    	// 检查调和是否已完成
    	if (workInProgress !== null) {
    		// 未完成
    		return RootInProgress; // 返回一个状态值,表示还有未完成
    	} else {
    		// 已完成
    		workInProgressRoot = null; // 重置root
        workInProgressRootRenderLanes = NoLanes; // 重置Lane
        finishQueueingConcurrentUpdates(); // 处理队列中的并发更新
        return workInProgressRootExitStatus; // 返回当前渲染root的最终退出状态
    	}
    }

    第二阶段:Commit(提交)

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

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

    1、遍历副作用列表:BeforeMutation

    // packages/react-reconciler/src/ReactFiberCommitWork.js
    // 以下只是核心逻辑的代码,不是commitBeforeMutationEffects的完整源码
    export function commitBeforeMutationEffects(
      root: FiberRoot,
      firstChild: Fiber,
    ): boolean {
      nextEffect = firstChild; // nextEffect是遍历此链表时的当前fiber
      commitBeforeMutationEffects_begin(); // 遍历fiber,处理节点删除和确认节点在before mutation阶段是否有要处理的副作用
     
      const shouldFire = shouldFireAfterActiveInstanceBlur; // 当一个焦点元素被删除或隐藏时,它会被设置为 true
      shouldFireAfterActiveInstanceBlur = false;
      focusedInstanceHandle = null;
     
      return shouldFire;
    }

    2、正式提交:CommitMutation

    // packages/react-reconciler/src/ReactFiberCommitWork.js
    // 以下只是核心逻辑的代码,不是commitMutationEffects的完整源码
    export function commitMutationEffects(
      root: FiberRoot,
      finishedWork: Fiber,
      committedLanes: Lanes,
    ) {
    	// lanes和root被设置为"in progress"状态,表示它们正在被处理
      inProgressLanes = committedLanes;
      inProgressRoot = root;
     
    	// 递归遍历Fiber,更新副作用节点
      commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
     
    	// 重置进行中的lanes和root
      inProgressLanes = null;
      inProgressRoot = null;
    }

    3、处理layout effects:commitLayout

    // packages/react-reconciler/src/ReactFiberCommitWork.js
    export function commitLayoutEffects(
      finishedWork: Fiber,
      root: FiberRoot,
      committedLanes: Lanes,
    ): void {
      inProgressLanes = committedLanes;
      inProgressRoot = root;
     
    	// 创建一个current指向就Fiber树的alternate
      const current = finishedWork.alternate;
    	// 处理那些由useLayoutEffect创建的layout effects
      commitLayoutEffectOnFiber(root, current, finishedWork, committedLanes);
     
      inProgressLanes = null;
      inProgressRoot = null;
    }

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

    结语

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