优秀的编程知识分享平台

网站首页 > 技术文章 正文

揭秘 React Reconciler 的工作流程

nanyue 2024-10-01 13:12:53 技术文章 6 ℃

默认大家多多少少都是了解过React(^16 的版本中,React才使用 Fiber 架构来表示组件树和管理更新过程)。老规矩,废话不多说,直接 coding...

1、Reconciler 是个啥?

Reconciler 翻译过来就是协调器,它负责处理React元素的更新并在内部构建虚拟 DOM。从 JSX 到真实 UI 的过程包括以下几个关键阶段:

  • JSX 编译阶段:将 JSX 编译成 React.createElement 调用。
  • 创建虚拟 DOM:生成虚拟 DOM 对象。
  • 创建 Fiber 树:将虚拟 DOM 转换为 Fiber 树。
  • 协调(Reconciliation):比较新旧 Fiber 树,找出需要更新的部分。
  • 提交阶段(Commit Phase):将所有的更新应用到真实 DOM 中。
  • 渲染到真实 DOM:更新后的 DOM 反映在浏览器中。

这个过程在实际的React实现中要复杂得多,特别是在处理并发更新、调度优先级和性能优化方面。

下面是一个简化的示例代码,展示了从 JSX 到真实 UI 的过程:

// 1. JSX 语法
const element = <h1>Hello, world!</h1>;

// 2. 编译为 JavaScript
// const element = React.createElement('h1', null, 'Hello, world!');

// 3. 创建虚拟DOM
const virtualDOM = {
  type: 'h1',
  props: {
    children: 'Hello, world!'
  }
};

// 4. 创建 Fiber 树(简化)
function createFiber(node) {
  return {
    type: node.type,
    props: node.props,
    stateNode: null,
    child: null,
    sibling: null,
    return: null
  };
}

const fiberRoot = createFiber(virtualDOM);

// 5. 协调(简化)
function reconcile(fiber) {
  if (!fiber.stateNode) {
    // 创建真实DOM节点
    fiber.stateNode = document.createElement(fiber.type);
    fiber.stateNode.textContent = fiber.props.children;
  }
  // 递归处理子节点和兄弟节点
  if (fiber.child) {
    reconcile(fiber.child);
  }
  if (fiber.sibling) {
    reconcile(fiber.sibling);
  }
}

// 6. 提交阶段(简化)
function commitRoot(fiber) {
  document.getElementById('root').appendChild(fiber.stateNode);
}

// 执行协调和提交
reconcile(fiberRoot);
commitRoot(fiberRoot);

2、FiberNode 函数如何实现?

FiberNode是 Reconciler 的核心数据结构之一,用于构建协调树。Reconciler 使用 FiberNode 来表示 React 元素树中的节点,并通过比较 Fiber 树的差异,找出需要进行更新的部分,生成更新指令,来实现 UI 的渲染和更新。

既然是用来表示节点的,那我们来看看它包含了哪些重要的字段:

  • type:节点的类型,可以是原生 DOM 元素、函数组件或类组件等;
  • stateNode:节点对应的实际 DOM 节点或组件实例;
  • child:指向节点的第一个子节点;
  • sibling:指向节点的下一个兄弟节点;
  • return:指向节点的父节点;
  • alternate:指向节点的备份节点,用于在协调过程中进行比较(用于双缓冲机制);
  • pendingProps:表示节点的新属性,用于在协调过程中进行更新。
/**
 * 定义 FiberNode 构造函数
 * /packages/react-reconciler/src/ReactFiber.new.js
*/
function FiberNode(
  tag: WorkTag, // 节点的类型(例如,函数组件、类组件、DOM元素等)
  pendingProps: mixed, // 节点的新属性
  key: null | string, // 节点的唯一标识符,用于优化列表渲染
  mode: TypeOfMode, // 节点的模式(例如,严格模式、并发模式等)
) {
  // 实例属性初始化
  this.tag = tag; // 保存节点类型
  this.key = key; // 保存节点的唯一标识符
  this.elementType = null; // 保存节点的元素类型(用于函数组件)
  this.type = null; // 保存节点的类型(例如,DOM元素的标签名或组件函数)
  this.stateNode = null; // 保存与节点关联的实际DOM节点或类组件实例

  // Fiber属性初始化
  this.return = null; // 保存指向父Fiber节点的引用
  this.child = null; // 保存指向第一个子Fiber节点的引用
  this.sibling = null; // 保存指向下一个兄弟Fiber节点的引用
  this.index = 0; // 保存节点在兄弟节点中的索引位置

  this.ref = null; // 保存节点的ref属性,用于访问DOM节点或组件实例

  this.pendingProps = pendingProps; // 保存节点的新属性,用于在协调过程中进行更新
  this.memoizedProps = null; // 保存上一次渲染时的属性
  this.updateQueue = null; // 保存节点的更新队列,用于管理状态更新和副作用
  this.memoizedState = null; // 保存上一次渲染时的状态
  this.dependencies = null; // 保存节点的依赖项(用于Context等)

  this.mode = mode; // 保存节点的模式(例如,严格模式、并发模式等)

  // 副作用相关属性初始化
  this.flags = NoFlags; // 保存节点的副作用标志
  this.subtreeFlags = NoFlags; // 保存子树的副作用标志
  this.deletions = null; // 保存需要删除的子节点

  this.lanes = NoLanes; // 保存节点的更新优先级
  this.childLanes = NoLanes; // 保存子节点的更新优先级

  this.alternate = null; // 保存节点的备用Fiber,用于双缓冲机制

  // 如果启用了 Profiler 计时器
  if (enableProfilerTimer) {
    // 注意:以下操作是为了避免 V8 性能问题
    //
    // 初始化下面的字段为 double 类型,避免 Fibers 具有不同的形状。
    // 这与 Object.preventExtension() 有关,
    // 这种性能问题只影响开发环境,但会使 React 变得非常慢。
    // 解决方法是初始化为 double 类型。
    //
    // 更多信息请见:
    // https://github.com/facebook/react/issues/14365
    // https://bugs.chromium.org/p/v8/issues/detail?id=8538
    this.actualDuration = Number.NaN; // 记录实际渲染持续时间
    this.actualStartTime = Number.NaN; // 记录实际渲染开始时间
    this.selfBaseDuration = Number.NaN; // 记录自身基础持续时间
    this.treeBaseDuration = Number.NaN; // 记录子树基础持续时间

    // 可以在初始化后将 double 类型替换为其他类型,不会触发上述性能问题
    // 这样可以简化其他分析器代码(包括开发者工具)
    this.actualDuration = 0; // 初始化实际渲染持续时间
    this.actualStartTime = -1; // 初始化实际渲染开始时间
    this.selfBaseDuration = 0; // 初始化自身基础持续时间
    this.treeBaseDuration = 0; // 初始化子树基础持续时间
  }

  // 如果在开发环境中
  if (__DEV__) {
    // 以下字段不直接使用,但有助于调试内部机制

    this._debugSource = null; // 保存调试信息的源代码位置
    this._debugOwner = null; // 保存调试信息的拥有者组件
    this._debugNeedsRemount = false; // 标记是否需要重新挂载
    this._debugHookTypes = null; // 保存调试信息的Hook类型
    if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
      Object.preventExtensions(this); // 防止扩展当前对象,帮助调试
    }
  }
}

3、Reconciler 的工作流程

React Fiber Reconciler

Reconciler 的工作流程总的来说就是对 Fiber 树进行一次 深度优先遍历(DFS) ,首先访问根节点,然后依次访问左子树和右子树,通过比较新节点(新生成的 React Element)和旧节点(现有的 FiberNode),生成更新计划,并打上不同的标记。

  1. 遍历 Fiber 树: React 使用深度优先搜索(DFS)算法来遍历 Fiber 树,首先会从 Fiber 树的根节点开始进行遍历,遍历整个组件树的结构。
  2. 比较新旧节点: 对于每个 Fiber 节点,Reconciler 会比较新节点(即新的 React Element)和旧节点(即现有的 FiberNode)之间的差异,比较的内容包括节点类型、属性、子节点等方面的差异。
  3. 生成更新计划: 根据比较的结果,Reconciler 会生成一个更新计划,用于确定需要进行的操作,更新计划通常包括哪些节点需要更新、哪些节点需要插入到 DOM 中、哪些节点需要删除等信息。
  4. 打标记(Tagging): 为了记录不同节点的操作,React 会为每个节点打上不同的标记。例如,如果节点需要更新,可能会打上更新标记(Update Tag);如果节点是新创建的,可能会打上插入标记(Placement Tag);如果节点被移除,可能会打上删除标记(Deletion Tag)等。
  5. 更新 Fiber 节点: 根据生成的更新计划和标记,Reconciler 会更新对应的 Fiber 节点以反映组件的最新状态。更新操作可能包括更新节点的状态、更新节点的属性、调用生命周期方法等。
  6. 递归处理子节点: 对于每个节点的子节点,React 会递归地重复进行上述的比较和更新操作,以确保整个组件树都得到了正确的处理。

当所有 React Element 都比较完成之后,会生成一棵新的 Fiber 树,此时,一共存在两棵 Fiber 树:

  • current: 与视图中真实 UI 对应的 Fiber 树,当 React 开始新的一轮渲染时,会使用 current 作为参考来比较新的树与旧树的差异,决定如何更新 UI;
  • workInProgress: 触发更新后,正在 Reconciler 中计算的 Fiber 树,一旦 workInProgress 上的更新完成,它将会被提交为新的 current,成为下一次渲染的参考树,并清空旧的 current 树。

下面我们来看看 Reconciler 相关函数的具体实现: 查看所有源码,猛戳 /packages/react-reconciler/src/ReactFiberWorkLoop.new.js

// 面试官:renderRootSync和renderRootConcurrent有什么区别,各自适合什么场景,如何触发?

/**
 * 并发渲染根节点
 * 该函数用于处理React的并发渲染,负责调度和执行高优先级的渲染任务,
 * 并在必要时处理异常。
 */
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
  // 保存当前的执行上下文
  const prevExecutionContext = executionContext;
  // 设置执行上下文为渲染上下文
  executionContext |= RenderContext;
  // 推入调度器,并保存之前的调度器
  const prevDispatcher = pushDispatcher();

  // 如果当前工作中的根节点或者渲染优先级发生了变化,需要重新准备渲染栈
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    // 如果启用了更新跟踪且开发者工具存在
    if (enableUpdaterTracking) {
      if (isDevToolsPresent) {
        // 获取 memoizedUpdaters(已记忆的更新器)
        const memoizedUpdaters = root.memoizedUpdaters;
        // 如果有未处理的更新器
        if (memoizedUpdaters.size > 0) {
          // 恢复挂起的更新器,并清空 memoizedUpdaters
          restorePendingUpdaters(root, workInProgressRootRenderLanes);
          memoizedUpdaters.clear();
        }

        // 将调度的 Fiber 移动到 memoized 中
        movePendingFibersToMemoized(root, lanes);
      }
    }

    // 获取渲染优先级对应的 transitions
    workInProgressTransitions = getTransitionsForLanes(root, lanes);
    // 重置渲染计时器
    resetRenderTimer();
    // 准备一个新的渲染栈
    prepareFreshStack(root, lanes);
  }

  // 如果是开发环境,并且启用了调试追踪
  if (__DEV__) {
    if (enableDebugTracing) {
      // 记录渲染开始的日志
      logRenderStarted(lanes);
    }
  }

  // 如果启用了调度分析器,标记渲染已开始
  if (enableSchedulingProfiler) {
    markRenderStarted(lanes);
  }

  // 开始主要的渲染工作循环
  do {
    try {
      // 执行并发渲染的主循环
      workLoopConcurrent();
      // 成功执行则退出循环
      break;
    } catch (thrownValue) {
      // 如果出现异常,处理错误
      handleError(root, thrownValue);
    }
  } while (true);

  // 重置上下文依赖关系
  resetContextDependencies();

  // 弹出调度器,恢复之前的调度器状态
  popDispatcher(prevDispatcher);
  // 恢复之前的执行上下文
  executionContext = prevExecutionContext;

  // 如果是开发环境,并且启用了调试追踪
  if (__DEV__) {
    if (enableDebugTracing) {
      // 记录渲染结束的日志
      logRenderStopped();
    }
  }

  // 检查树是否已经完成渲染
  if (workInProgress !== null) {
    // 如果仍有工作待完成,标记渲染已暂停
    if (enableSchedulingProfiler) {
      markRenderYielded();
    }
    // 返回树仍在进行中的状态
    return RootInProgress;
  } else {
    // 如果树已完成渲染
    if (enableSchedulingProfiler) {
      markRenderStopped();
    }

    // 清空当前工作中的根节点
    workInProgressRoot = null;
    workInProgressRootRenderLanes = NoLanes;

    // 返回最终的退出状态
    return workInProgressRootExitStatus;
  }
}
/**
 * 准备一个新的渲染堆栈,用于处理新的更新
 * 该函数重置与上一次渲染相关的状态,并为当前根节点创建一个新的工作副本,
 * 以便开始处理新的渲染任务。
 */
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
  // 清空上一次渲染完成的工作和优先级
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  // 获取根节点的超时句柄
  const timeoutHandle = root.timeoutHandle;
  if (timeoutHandle !== noTimeout) {
    // 如果根节点之前暂停并调度了一个超时来提交回退状态
    // 现在有了额外的工作,取消超时
    root.timeoutHandle = noTimeout;
    // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
    cancelTimeout(timeoutHandle);
  }

  // 如果有正在进行的工作
  if (workInProgress !== null) {
    // 获取被中断工作的上一级节点
    let interruptedWork = workInProgress.return;
    // 循环处理被中断的工作
    while (interruptedWork !== null) {
      // 获取当前节点的副本
      const current = interruptedWork.alternate;
      // 处理被中断的工作
      unwindInterruptedWork(
        current,
        interruptedWork,
        workInProgressRootRenderLanes,
      );
      // 继续向上一级节点移动
      interruptedWork = interruptedWork.return;
    }
  }

  // 设置当前正在进行的根节点
  workInProgressRoot = root;
  // 创建根节点的工作副本
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  // 设置当前正在进行的工作节点
  workInProgress = rootWorkInProgress;
  // 设置当前渲染优先级和其他相关状态
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  workInProgressRootExitStatus = RootInProgress;
  workInProgressRootFatalError = null;
  workInProgressRootSkippedLanes = NoLanes;
  workInProgressRootInterleavedUpdatedLanes = NoLanes;
  workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
  workInProgressRootPingedLanes = NoLanes;
  workInProgressRootConcurrentErrors = null;
  workInProgressRootRecoverableErrors = null;

  // 完成并发更新队列
  finishQueueingConcurrentUpdates();

  // 如果是开发环境,丢弃待处理的严格模式警告
  if (__DEV__) {
    ReactStrictModeWarnings.discardPendingWarnings();
  }

  // 返回根节点的工作副本
  return rootWorkInProgress;
}
/**
 * 深度优先遍历,向下递归子节点
 */
function workLoopConcurrent() {
  // 执行工作,直到调度器要求我们暂停
  while (workInProgress !== null && !shouldYield()) {
    // 执行一个工作单元
    performUnitOfWork(workInProgress);
  }
}
/**
 * 该函数负责执行一个fiber节点的工作。它会根据当前的fiber状态调用beginWork,
 * 如果该fiber没有生成新的工作,则调用completeUnitOfWork来完成当前的工作。
 */
function performUnitOfWork(unitOfWork: Fiber): void {
  // 当前fiber的已刷新状态是alternate。理想情况下,不应依赖此状态,
  // 但在这里依赖它意味着我们不需要在工作进度中添加额外的字段。
  const current = unitOfWork.alternate;
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  // 如果启用了Profiler计时器且当前fiber处于Profile模式
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    // 启动Profiler计时器
    startProfilerTimer(unitOfWork);
    // 调用beginWork开始处理当前fiber
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    // 停止Profiler计时器并记录时间差
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    // 调用beginWork开始处理当前fiber
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }

  resetCurrentDebugFiberInDEV();
  // 将当前fiber的待处理属性记忆到memoizedProps
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // 没有子节点,则遍历兄弟节点或父节点
    completeUnitOfWork(unitOfWork);
  } else {
    // 有子节点,继续向下深度遍历
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}
/**
 * 该函数尝试完成当前的工作单元,然后移动到下一个兄弟fiber。
 * 如果没有更多的兄弟fiber,则返回到父fiber。
 */
function completeUnitOfWork(unitOfWork: Fiber): void {
  // 尝试完成当前工作单元,然后移动到下一个兄弟fiber。
  // 如果没有更多兄弟fiber,则返回到父fiber。
  let completedWork = unitOfWork;
  do {
    // 当前fiber的已刷新状态是alternate。理想情况下,不应依赖此状态,
    // 但在这里依赖它意味着我们不需要在工作进度中添加额外的字段。
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    // 检查工作是否完成或是否有异常抛出。
    if ((completedWork.flags & Incomplete) === NoFlags) {
      setCurrentDebugFiberInDEV(completedWork);
      let next;
      // 如果未启用Profiler计时器或当前fiber不处于Profile模式
      if (!enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode) {
        // 完成当前工作
        next = completeWork(current, completedWork, subtreeRenderLanes);
      } else {
        // 启动Profiler计时器
        startProfilerTimer(completedWork);
        // 完成当前工作
        next = completeWork(current, completedWork, subtreeRenderLanes);
        // 停止Profiler计时器并记录时间差
        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
      }
      resetCurrentDebugFiberInDEV();

      if (next !== null) {
        // 完成当前fiber生成了新的工作,处理下一个工作单元
        workInProgress = next;
        return;
      }
    } else {
      // 当前fiber未完成,因为有异常抛出。弹出栈中的值而不进入完成阶段。
      // 如果这是一个边界,尽可能捕获值。
      const next = unwindWork(current, completedWork, subtreeRenderLanes);

      // 因为当前fiber未完成,不要重置它的lanes。

      if (next !== null) {
        // 如果完成当前工作生成了新的工作,处理下一个工作单元。
        // 我们会再次回到这里。
        // 由于我们正在重启,移除任何不是宿主效果的effect tag。
        next.flags &= HostEffectMask;
        workInProgress = next;
        return;
      }

      if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
        // 记录发生异常的fiber的渲染持续时间。
        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);

        // 在继续之前,包含处理失败子节点的时间。
        let actualDuration = completedWork.actualDuration;
        let child = completedWork.child;
        while (child !== null) {
          actualDuration += child.actualDuration;
          child = child.sibling;
        }
        completedWork.actualDuration = actualDuration;
      }

      if (returnFiber !== null) {
        // 标记父fiber为未完成并清除其子树标志。
        returnFiber.flags |= Incomplete;
        returnFiber.subtreeFlags = NoFlags;
        returnFiber.deletions = null;
      } else {
        // 我们已经回到了根节点。
        workInProgressRootExitStatus = RootDidNotComplete;
        workInProgress = null;
        return;
      }
    }

    // 获取下一个兄弟fiber
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // 如果在当前returnFiber中有更多工作要做,则处理下一个工作单元。
      workInProgress = siblingFiber;
      return;
    }
    // 否则,返回到父fiber
    completedWork = returnFiber;
    // 更新当前工作单元以防止异常抛出。
    workInProgress = completedWork;
  } while (completedWork !== null);

  // 我们已经到达根节点。
  if (workInProgressRootExitStatus === RootInProgress) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

这几段代码实现了React渲染的工作循环,通过深度优先遍历Fiber树,处理和完成每个工作单元,确保 UI 的更新。

最后

有写错的,或者有什么问题,欢迎留言讨论 。

最近发表
标签列表