第 98 期 - 解读 React 19 源码中的 Diff 算法
logoFRONTALK AI/1月31日 16:31/阅读原文

摘要

作者因辅导学生面试重读 React 19 源码中的 Diff 算法,介绍函数缓存优化、Fiber 链表结构、深度优先遍历、更新机制等内容,还提及 Diff 算法对运用 Next.js 的影响以及对高级程序员的重要性。

一、函数缓存优化

在前端开发中,为避免函数重复执行耗时运算,可采用缓存优化。如定义cache对象用于缓存,假设expensive(a, b)运算耗时,在cul函数中,先判断入参是否与上次相同,相同则直接返回缓存结果,否则执行运算并缓存结果。React 底层更新机制类似,只是缓存内容从普通对象变为完整的 Fiber 链表。

const cache = {
  preA: null,
  preB: null,
  preResult: null
};
function cul(a, b) {
  if (cache.preA === a && cache.preB === b) {
    return cache.preResult;
  }
  cache.preA = a;
  cache.preB = b;
  const result = expensive(a, b);
  cache.preResult = result;
  return result;
}

二、Fiber 链表结构

Fiber 对象是 React 中存储入参和结果的缓存对象,与虚拟 DOM 不同,它是运行时上下文,其字段记录节点运行状态。FiberNode函数展示了 Fiber 的结构,其中returnchildsibling是构成 Fiber 链表的重要字段,return指向父节点,child指向子元素第一个节点,sibling指向兄弟节点。

function FiberNode(tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode) {
  // 静态数据结构
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null; // 指向真实 DOM 对象
  // 构建 Fiber 树的指针
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;
  this.ref = null;
  // 存储更新与状态
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  this.mode = mode;
  // 存储副作用回调
  this.effectTag = NoEffect;
  this.nextEffect = null;
  this.firstEffect = null;
  this.lastEffect = null;
  // 优先级
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  // 复用节点
  this.alternate = null;
}

三、深度优先遍历

<App>组件为例,语法糖形式下包含<Header />等子组件,实际运行时是函数执行过程,节点执行顺序满足函数调用栈的深度优先原则。

四、更新机制

React 每次更新是全量更新,从根节点开始,这是其被认为性能差的原因。但利用好diff规则可实现元素级细粒度更新,只是对开发者要求高。了解更新机制有助于在源码中找到每次更新的起点位置。

五、diff 起点

每次更新从根节点开始,在ReactFiberWorkLoop.js中的performWorkOnRoot方法。并发更新的旧方法是performConcurrentWorkOnRoot,同步更新是performSyncWorkOnRootperformWorkOnRoot方法中,根据条件选择执行renderRootConcurrentrenderRootSync,它们分别启动workLoopConcurrentworkLoopSync循环,最终都会执行performUnitOfWorkworkInProgress是全局上下文变量,表示当前正在被比较的节点,其值会不断变化。

function workLoopConcurrent() {
  while (workInProgress!== null &&!shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
function workLoopSync() {
  while (workInProgress!== null) {
    performUnitOfWork(workInProgress);
  }
}
function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode)!== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

六、beginWork 的作用

beginWork利用当前节点计算下一个节点,要关注其入参和返回值才能理解diff原理。在beginWork执行中,会比较当前节点的propscontext决定是否复用下一个节点。还涉及didReceiveUpdate全局变量,用于标记当前fiber节点是否复用子fiber节点,以及checkScheduledUpdateOrContext函数比较是否存在updatecontext变化。state的比较通过标记更新优先级等方式实现。当statepropscontext比较无变化时,直接进入bailout复用节点;否则根据tag执行不同创建函数。重点关注updateFunctionComponent中的reconcileChildren方法,它会调用reconcileChildFibers方法,这是子节点diff的入口函数,根据newChild类型做不同处理。以reconcileSingleElement为例,会比较keytypetag的值,相同则复用节点。

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() ||
      // Force a re - render if the implementation changed due to hot reload:
      (__DEV__? workInProgress.type!== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else {
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
        current,
        renderLanes
      );
      if (
      !hasScheduledUpdateOrContext &&
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes
        );
      }
    }
  }
  return null;
}

七、总结

本文从源码角度引导读者理解Diff算法,通过实现思路和函数调用路径分享,而非直接给出结论。读者可依此提炼知识点。同时提到利用Diff规则进行精准元素级别细粒度更新对项目性能的重要性,以及对运用Next.js的影响,强调这是高级程序员必须掌握的重点知识。

 

扩展阅读

Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有