普通视图

发现新文章,点击刷新页面。
昨天 — 2025年7月3日首页

React 渲染全流程剖析:初次渲染与重渲染的底层逻辑

作者 G扇子
2025年7月3日 10:38

附上Vue渲染机制以作对比:Vue3渲染机制解析:编译时优化与虚拟DOM的性能跃迁

一、初次渲染

import Image from './Image.js';
import { createRoot } from 'react-dom/client';

const root = createRoot(document.getElementById('root'))
root.render(<Image />);

如以上代码初次渲染是调用createRoot方法转入目标DOM节点,然后调用render函数完成的渲染,以下是详细步骤:

1. JSX编译成虚拟DOM树

通过babel编译转化jsxjs格式代码,如

return (
<MyButton color="blue" shadowSize={2}>
  Click Me
</MyButton>
)

转化为

return (
React.createElement(
  MyButton,
  {color: 'blue', shadowSize: 2},
  'Click Me'
)
)

createElement接受以下参数:

  • component:元素类型标签
  • props:标签属性
  • children:子节点 编译后的元素即是虚拟DOM树节点 注意render函数在类组件中即是指render方法,如果是在函数组件中则是函数组件本身,即
function Foo() {
    return <h1> Foo </h1>;
}

2. Fiber树构建

创建根节点

function createFiberRoot(containerInfo) {
  // 创建 FiberRoot(React应用根节点)
  const root = new FiberRootNode(containerInfo);
  
  // 创建未初始化的 HostRootFiber(Fiber树的根节点)
  const uninitializedFiber = createHostRootFiber();
  root.current = uninitializedFiber;  // FiberRoot.current 指向 HostRootFiber
  uninitializedFiber.stateNode = root; // HostRootFiber.stateNode 回指 FiberRoot
  
  // 初始化更新队列
  initializeUpdateQueue(uninitializedFiber);
  return root;
}

创建更新对象

function updateContainer(element, container) {
  const current = container.current; // 这里是HostRootFiber
  const lane = requestUpdateLane(current); // 获取更新优先级
  
  // 创建更新对象
  const update = createUpdate(lane);
  update.payload = { element }; // 存储 ReactElement (<App/>)
  
  // - 将根组件 <App/> 存入 HostRootFiber 的更新队列
  enqueueUpdate(current, update);
  
  // 开始调度更新
  scheduleUpdateOnFiber(current, lane);
}

// 更新后内存结构:
HostRootFiber.updateQueue.shared.pending = {
  payload: { element: <App/> }, // 存储要渲染的组件
  next: update // 环形链表
}

3. 协调阶段:深度优先构建Fiber树

  • workInProgress 指针跟踪当前处理的 Fiber
  • 此处无alternate 指针,因为是初次渲染,无旧树进行比较
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  
  // 创建子Fiber节点
  const next = beginWork(current, unitOfWork, renderLanes);
  
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  
  if (next === null) {
    // 完成当前节点
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next; // 继续处理子节点
  }
}

例子1:

class App extends React.Component {
  componentDidMount() {
    console.log(`App Mount`);
    console.log(`App 组件对应的fiber节点: `, this._reactInternals);
  }

  render() {
    return (
      <div className="app">
        <header>header</header>
        <Content />
      </div>
    );
  }
}

class Content extends React.Component {
  componentDidMount() {
    console.log(`Content Mount`);
    console.log(`Content 组件对应的fiber节点: `, this._reactInternals);
  }

  render() {
    return (
      <React.Fragment>
        <p>1</p>
        <p>2</p>
      </React.Fragment>
    );
  }
}

export default App;

例子1中Fiber 树构建过程: 1. 从 HostRootFiber 开始,创建 <App> Fiber 节点 2. 处理 <App> 的 render 结果,创建 <div> Fiber 节点 3. 处理 <div> 的子节点,依次创建 <header> 和 <Content> Fiber 节点 4. 处理 <Content> 的 render 结果,创建 <Fragment> 和两个 <p> Fiber 节点

4. 完成阶段:创建DOM和收集副作用

  • 所有标记了 Placement 的 Fiber 组成单向链表
  • 顺序:深度优先,子节点在前,父节点在后
function completeWork(current, workInProgress) {
  switch (workInProgress.tag) {
    case HostComponent:
      // 创建DOM实例
      const instance = createInstance(workInProgress.type, workInProgress.pendingProps);
      
      // 将子DOM附加到当前DOM
      appendAllChildren(instance, workInProgress);
      
      // 设置DOM属性
      finalizeInitialChildren(instance, workInProgress.type, workInProgress.pendingProps);
      
      workInProgress.stateNode = instance; // 关联DOM
      
      // 标记插入操作
      if (workInProgress.flags & Placement) {
        markUpdate(workInProgress);
      }
      break;
    case HostText:
      // 文本节点处理...
    // 其他类型...
  }
  
  // 收集副作用到父节点
  if (workInProgress.flags > PerformedWork) {
    appendEffectToList(workInProgress.return, workInProgress);
  }
}

例子1的副作用链表的形成:<header><p>1 → <p>2 → <div><App>HostRootFiber

5. 提交阶段:DOM操作

function commitRoot(root) {
  const finishedWork = root.finishedWork;
  let effect = finishedWork.firstEffect;
  
  // 提交所有插入操作
  while (effect !== null) {
    commitPlacement(effect); // 实际DOM插入操作
    effect = effect.nextEffect;
  }
  
  // 调用生命周期
  commitLifeCycles(finishedWork);
}

image.png

二、重渲染

组件(或者是其祖先之一)的状态发生了改变会导致重新渲染。

1. 状态变更触发更新

创建更新对象

  • 当组件状态变更(通过 setStateuseStateforceUpdate)时,React 会创建一个更新对象(update object)。这个对象包含更新内容、优先级信息等,并添加到对应 Fiber 节点的更新队列中
  • 当使用setState时:
enqueueSetState(inst, payload) {
  const fiber = getInstance(inst); // 获取组件对应fiber
  const lane = requestUpdateLane(fiber); // 确定更新优先级
  const update = createUpdate(eventTime, lane);
  update.payload = payload; // 存储更新数据
  
  // 将更新加入fiber的更新队列
  enqueueUpdate(fiber, update);
  
  // 开始调度
  scheduleUpdateOnFiber(fiber, lane);
}

标记更新路径

  • React 需要确定哪些节点会受到更新的影响,它会从触发更新的 Fiber 节点开始,向上遍历父节点,标记出整个更新路径
  • 更新路径可帮助后续遍历跳过未标记的虚拟DOM子树
function markUpdateLaneFromFiberToRoot(sourceFiber, lane) {
  // 设置当前fiber的lanes(标记当前节点需要更新)
  sourceFiber.lanes = mergeLanes(sourceFiber.lanes, lane);
  
  // 向上遍历父节点,设置childLanes(标记子树需要更新)
  let node = sourceFiber;
  let parent = sourceFiber.return;
  while (parent !== null) {
    parent.childLanes = mergeLanes(parent.childLanes, lane);
    node = parent;
    parent = parent.return;
  }
  
  // 返回FiberRoot
  return node.stateNode;
}

2. 准备阶段:双缓存结构初始化

  • 使用双缓存技术,在更新时创建一棵新的 workInProgress 树,与 current 树(当前显示树)交替使用
function prepareFreshStack(root, lanes) {
  // 创建workInProgress树
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  
  // 初始化workInProgress节点
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  workInProgress = rootWorkInProgress;
  workInProgressRoot = root;
  workInProgressRootRenderLanes = lanes;
}

3. 协调阶段:对比更新

beginWork与节点对比

  • 在 beginWork 阶段,React 对比新旧节点,决定是否需要更新或复用子树
function beginWork(current, workInProgress, renderLanes) {
  if (current !== null) {
    // 检查props是否变化
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    
    // 判断是否需要更新
    if (oldProps === newProps && !hasLegacyContextChanged()) {
      // 检查优先级:是否在本次渲染范围内
      if (!includesSomeLane(renderLanes, workInProgress.lanes)) {
        // 无需更新,进入bailout逻辑
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
      }
    }
    didReceiveUpdate = true;
  }
  
  // 需要更新:调用具体updatexxx更新函数
  switch (workInProgress.tag) {
    case ClassComponent: 
      return updateClassComponent(current, workInProgress, ...);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    // 其他类型...
  }
}

bailout 逻辑:跳过未变更子树

  • 当节点不需要更新时,React进入bailout逻辑
  • bailout 条件:props 未变化且优先级不匹配
  • 根据更新路径可以跳过整个子树的重渲染
function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
  // 检查子节点是否需要更新
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // 整个子树都不需要更新,直接跳过
    return null;
  }
  
  // 克隆子节点(复用现有 Fiber)
  cloneChildFibers(current, workInProgress);
  
  // 返回第一个子节点继续处理
  return workInProgress.child;
}

4. updatexxx函数与reconcileChildren调和函数(此时需要更新)

当节点需要更新时,React 会调用调和函数,其实现的 Diff 算法会对比新旧子节点,决定复用、移动或删除节点。 传统的Diff算法是循环递归每一个节点(真实DOM节点),算法复杂度是O(n^3)。Vue和React都使用虚拟DOM节点的Diff算法,虚拟DOM是将目标所需的UI通过数据结构虚拟表现出来,保存到内存中,再将真实DOM与之保持同步。 Vue2的Diff算法使用双端对比虚拟节点,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作;Vue3在从=此基础借鉴了 ivi算法和 inferno算法。在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型。 React的Diff算法遵循以下三个层级的策略:tree Diff(只比较同层级节点,不跨层级比较)、component Diff(相同类型组件复用实例)、element Diff(使用 key 标识稳定元素)。本节不对diff算法展示细说,后面会出一篇新文章diff算法,到时候附上链接。 本节只需要了解reconcileChildren调和函数:

  • 调和函数是updateXXX(如: updateHostRoot, updateClassComponent 等)函数中的一项重要逻辑, 它的作用是向下生成子节点, 并设置fiber.flags. 与初次渲染对比
  • 初次创建时fiber节点没有比较对象, 所以在向下生成子节点的时候没有任何多余的逻辑
  • 对比更新时需要把ReactElement对象与旧fiber对象进行比较, 来判断是否需要复用旧fiber对象. 调和函数的目的
  1. 给新增,移动,和删除节点设置fiber.flags,flags标记:
    • Placement:新增或移动节点
    • Update:更新属性
    • Deletion:删除节点
  2. 如果是需要删除的fiber, 除了自身打上Deletion之外, 还要将其添加到父节点的effects链表中(正常副作用队列的处理是在completeWork函数, 但是该节点(被删除)会脱离fiber树, 不会再进入completeWork阶段, 所以在beginWork阶段提前加入副作用队列)

5. 完成阶段:completeWork与副作用收集

在 completeWork 阶段,React 完成节点处理并收集副作用(即DOM 操作)。 completeWork函数与初次更新时的completeWork函数逻辑一致,只是此时current不为null

function completeWork(current, workInProgress) {
  const newProps = workInProgress.pendingProps;
  
  switch (workInProgress.tag) {
    case HostComponent: // DOM 元素
      if (current !== null && workInProgress.stateNode != null) {
        // 对比更新:比较新旧属性
        updateHostComponent(current, workInProgress, newProps);
      } else {
        // 新增节点:创建 DOM 实例
        const instance = createInstance(
          workInProgress.type,
          newProps,
          workInProgress
        );
        // 关联 DOM 与 Fiber
        workInProgress.stateNode = instance;
        // 设置初始属性
        finalizeInitialChildren(instance, newProps);
      }
      break;
    // 其他类型处理...
  }
  
  // 收集副作用(flags)
  if (workInProgress.flags > PerformedWork) {
    // 添加到父节点的副作用链表
    if (returnFiber.firstEffect === null) {
      returnFiber.firstEffect = workInProgress;
    } else {
      returnFiber.lastEffect.nextEffect = workInProgress;
    }
    returnFiber.lastEffect = workInProgress;
  }
}

触发updateHostComponent函数更新DOM属性

function updateHostComponent(current, workInProgress, newProps) {
  const oldProps = current.memoizedProps;
  
  // 比较新旧属性差异
  if (oldProps !== newProps) {
    // 计算属性差异
    const updatePayload = prepareUpdate(
      workInProgress.stateNode,
      workInProgress.type,
      oldProps,
      newProps
    );
    
    // 存储差异到 updateQueue
    workInProgress.updateQueue = updatePayload;
    
    // 标记 Update 副作用
    if (updatePayload) {
      workInProgress.flags |= Update;
    }
  }
}

6. 提交阶段:DOM 操作与生命周期

提交阶段遍历副作用链表,执行 DOM 操作并调用生命周期方法

function commitRoot(root) {
  const finishedWork = root.finishedWork;
  let effect = finishedWork.firstEffect;
  
  // 阶段1: BeforeMutation(调用 getSnapshotBeforeUpdate 获取 DOM 快照)
  commitBeforeMutationEffects();
  
  // 阶段2: Mutation(DOM操作)
  while (effect !== null) {
    const nextEffect = effect.nextEffect;
    const flags = effect.flags;
    
    // 处理 Placement(插入/移动)
    if (flags & Placement) {
      commitPlacement(effect);
    } 
    // 处理 Update(属性更新)
    else if (flags & Update) {
      commitWork(effect);
    }
    // 处理 Deletion(删除)
    else if (flags & Deletion) {
      commitDeletion(effect);
    }
    
    effect = nextEffect;
  }
  
  // 阶段3: Layout(同步生命周期)
  effect = finishedWork.firstEffect;
  while (effect !== null) {
    const nextEffect = effect.nextEffect;
    if (effect.flags & Update) {
      // 类组件:componentDidMount/Update
      // 函数组件:useLayoutEffect
      commitLayoutEffects(effect);
    }
    effect = nextEffect;
  }
  
  // 阶段4: Passive(异步 useEffect)
  scheduleCallback(NormalPriority, () => {
    flushPassiveEffects();
  });
}

7. 清理与切换:完成更新

提交完成后,React 清理临时状态并切换 current 指针

function finishCommit() {
  // 清理临时状态
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  
  // 切换 current 树,使新树成为当前树
  root.current = finishedWork;
  
  // 调度未处理的更新
  if (root.pendingPassiveEffects !== null) {
    scheduleCallback(NormalPriority, () => {
      flushPassiveEffects();
      return null;
    });
  }
  
  // 检查是否有待处理的更新
  ensureRootIsScheduled(root);
}

image.png

三、初次缓存和重缓存的区别

  • 对于初次渲染, React 会调用根组件。
  • 对于重渲染, React 会调用内部状态更新触发了渲染的函数组件
    • 这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
特性 初次渲染 重渲染
缓存机制 无缓存 双缓存树(current/workInProgress)
节点创建 全量新建节点 复用节点 + 条件创建新节点
对比机制 无旧节点比较 Fiber节点Diff算法(复用决策)
副作用标记 全部标记为Placement 动态标记Update/Placement/Deletion
子树处理 全量处理 bailout机制跳过未变更子树
DOM操作 全量插入 增量更新(仅变更部分)
性能优化 无优化空间 优先级调度 + 子树跳过
alternate指针 不存在 新旧节点互相引用

Clipboard_Screenshot_1751456908.png

四、React和Vue的渲染机制的区别

1. 虚拟DOM

  • React:通过JSX编译生成虚拟DOM树,所有UI逻辑(插值、循环、条件)均用原生JavaScript实现。状态变化时生成新虚拟DOM树,通过Diff算法 计算最小变更集,再更新真实DOM
  • Vue:模板编译为虚拟DOM树,但依赖响应式系统追踪数据变化。数据变更时直接定位受影响组件,生成局部虚拟DOM并比对,减少不必要的树遍历

2. 响应式系统

  • React无自动依赖追踪。状态更新(setState/useState)默认触发当前组件及所有子组件的重渲染,需开发者手动优化(如 React.memo 或 shouldComponentUpdate
  • Vue依赖收集+发布订阅
    • Vue2:使用 Object.defineProperty 拦截数据读写
    • Vue3:换成使用Proxy实现细粒度依赖追踪,数据变更时仅触发关联组件的更新

参考资料: fiber 树构造(初次创建) fiber 树构造(对比更新) React文档-渲染和提交

❌
❌