【react18原理探究实践】render阶段【首次挂载】

🧑‍💻 前言

在上一篇文章里,我们从 scheduleUpdateOnFiber 出发,跟踪到 React 内部如何触发 调度更新

调度完成后,更新会进入 render 阶段

render 阶段的核心目标是:基于最新的 props、state,构建一棵全新的 Fiber 树,并在完成阶段生成 DOM 节点

本篇文章我们将从 首次挂载流程 出发,结合源码逐步拆解,理解 React 在没有 Diff 复用的情况下,如何一步步"生造"出一棵 Fiber 树并对应的 DOM。


1. Render 阶段全景

先整体感受一下 render 阶段的 流程图

ts 复制代码
Render 流程:
│
├─ 初始化阶段
│  ├─ ReactDOM.render(<App />, container)
│  ├─ 创建 FiberRoot 与 HostRootFiber
│  └─ scheduleUpdateOnFiber(HostRootFiber)
│
├─ 递阶段(beginWork)
│  ├─ performUnitOfWork(workInProgress)
│  ├─ beginWork(current, workInProgress)
│  │   ├─ HostRoot → updateHostRoot()
│  │   ├─ ClassComponent → constructClassInstance()
│  │   ├─ FunctionComponent → renderWithHooks()
│  │   └─ HostComponent → 创建子Fiber
│  └─ 向下遍历子节点
│
├─ 归阶段(completeUnitOfWork)
│  ├─ completeWork(current, workInProgress)
│  │   ├─ HostComponent → createInstance()
│  │   ├─ HostText → createTextInstance()
│  │   ├─ appendAllChildren()
│  │   └─ finalizeInitialChildren()
│  └─ 向上回溯父节点
│
└─ Fiber 树构建完成
   └─ 等待 commit 阶段插入 DOM

一句话总结:Render = beginWork 向下递 + completeWork 向上归


2. 初始化阶段

当我们调用:

ts 复制代码
root.render(<App />, document.getElementById('root'))

流程是这样的:

  1. 创建 FiberRoot,内部保存容器信息(containerInfo = #root)。
  2. 创建 HostRootFiber,作为 Fiber 树的根。
  3. <App /> 作为一个 update 放进 HostRootFiber.updateQueue
  4. 调度更新 → scheduleUpdateOnFiber(HostRootFiber)

此时,调度器会在合适的时机执行 render 阶段。


3. beginWork ------ 递阶段

render 阶段从 根节点(HostRootFiber) 出发,调用 performUnitOfWork

js 复制代码
function performUnitOfWork(unitOfWork) {
  // 1. 进入递阶段
  const next = beginWork(unitOfWork.alternate, unitOfWork, renderLanes);

  // 2. 保存 props
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  // 3. 返回子节点(如果有)
  return next;
}

3.1 HostRoot → updateHostRoot

js 复制代码
function updateHostRoot(current, workInProgress, renderLanes) {
  // 处理 updateQueue,拿到 element
  processUpdateQueue(workInProgress, nextProps, null, renderLanes);

  const nextState = workInProgress.memoizedState;
  const nextChildren = nextState.element; // <App />

  // 协调子节点,创建 AppFiber
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);

  return workInProgress.child; // 返回 AppFiber
}

首次挂载时:

  • current 为 null
  • reconcileChildren 内部调用 mountChildFibers,直接创建新 Fiber,不做 diff。

3.2 ClassComponent → constructClassInstance

js 复制代码
function updateClassComponent(current, workInProgress, Component, nextProps) {
  if (workInProgress.stateNode === null) {
    // 🔥 首次挂载:创建实例
    constructClassInstance(workInProgress, Component, nextProps);
    mountClassInstance(workInProgress, Component, nextProps);
  }

  // 调用 render() 得到 children
  const nextChildren = workInProgress.stateNode.render();
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);

  return workInProgress.child;
}

特点:

  • new Component(props) 创建实例。
  • 执行 render(),得到 React 元素树。
  • 继续递归子节点。

3.3 FunctionComponent → renderWithHooks

js 复制代码
function updateFunctionComponent(current, workInProgress, Component, nextProps) {
  // 首次挂载:设置 Hook 调度器
  let nextChildren = renderWithHooks(current, workInProgress, Component, nextProps);
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);

  return workInProgress.child;
}

特点:

  • 使用 HooksDispatcherOnMount
  • 执行函数组件 Component(nextProps)
  • 构建 Hook 链表(useState、useEffect)。

3.4 HostComponent → DOM 容器

js 复制代码
function updateHostComponent(current, workInProgress, renderLanes) {
  const nextProps = workInProgress.pendingProps;
  const nextChildren = nextProps.children;

  // 协调子节点
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);

  return workInProgress.child;
}

特点:

  • Fiber 本身没有立即创建 DOM。
  • 只负责生成子 Fiber(例如 div 下的子节点)。

4. completeUnitOfWork ------ 归阶段

递归到最深的子节点后,进入 归阶段

js 复制代码
function completeUnitOfWork(unitOfWork) {
  do {
    const current = unitOfWork.alternate;
    const returnFiber = unitOfWork.return;

    // 调用 completeWork
    completeWork(current, unitOfWork, subtreeRenderLanes);

    // 如果有兄弟,切换到兄弟
    if (unitOfWork.sibling !== null) {
      workInProgress = unitOfWork.sibling;
      return;
    }

    // 否则回到父节点
    unitOfWork = returnFiber;
    workInProgress = unitOfWork;
  } while (unitOfWork !== null);
}

5. completeWork ------ 创建 DOM

核心逻辑:

js 复制代码
function completeWork(current, workInProgress, renderLanes) {
  switch (workInProgress.tag) {
    case HostComponent: {
      if (current === null) {
        // 🔥 首次挂载:创建 DOM
        const instance = createInstance(type, newProps, rootContainer, context);
        appendAllChildren(instance, workInProgress);
        finalizeInitialChildren(instance, type, newProps, rootContainer, context);

        workInProgress.stateNode = instance;
      }
      return null;
    }

    case HostText: {
      if (current === null) {
        workInProgress.stateNode = createTextInstance(newProps);
      }
      return null;
    }

    // 组件类节点(ClassComponent、FunctionComponent)不创建 DOM
  }
}

执行要点:

  1. createInstancedocument.createElement(type)
  2. appendAllChildren → 遍历子 Fiber,找到 HostComponent/HostText,appendChild
  3. finalizeInitialChildren → 设置属性、事件绑定。
  4. 保存到 fiber.stateNode

6. 流程小结

以一个例子:

js 复制代码
function App() {
  return <div className="app"><p>Hello</p></div>;
}
ReactDOM.render(<App />, root);

执行过程:

  1. HostRoot → 创建 AppFiber。
  2. AppFiber → 执行 App() → 返回 <div>
  3. divFiber → 创建 pFiber。
  4. pFiber → 创建 TextFiber。
  5. 归阶段 → 从 TextFiber 开始,逐层 createTextNode → createElement → appendChild
  6. 最终,Fiber 树构建完成,DOM 树也就绪。

📌 总结

React Render 首次挂载流程:

  1. 初始化:创建 FiberRoot 和 HostRootFiber,调度更新。
  2. 递阶段(beginWork) :向下遍历,基于组件类型创建子 Fiber。
  3. 归阶段(completeWork) :向上回溯,创建 DOM 实例,收集副作用。
  4. 完成 Fiber 树:等待 commit 阶段将 DOM 插入页面。

一句话总结:

👉 首次挂载时,render 阶段会完整走一遍 Fiber 树递归,创建所有 Fiber 和 DOM 实例,不存在 Diff 复用

相关推荐
ObjectX前端实验室2 小时前
【react18原理探究实践】组件的 props 和 state 究竟是如何确定和存储的?
前端·react.js
fxshy3 小时前
解决 Web 应用加载地图资源时的 HTTP 与 HTTPS 混合内容问题
前端·网络协议·http
一个很帅的帅哥3 小时前
Vue keep-alive
前端·javascript·vue.js·keep-alive
lbh3 小时前
Chrome DevTools 详解(一):Elements 面板
前端·javascript·浏览器
明里人3 小时前
React 状态库:Zustand 和 Jotai 怎么选?
前端·javascript·react.js
sniper_fandc3 小时前
Vue3双向数据绑定v-model
前端·vue
訾博ZiBo4 小时前
为什么我的 React 组件会无限循环?—— 一次由 `onClick` 引发的“惨案”分析
前端·react.js
訾博ZiBo4 小时前
React状态更新之谜:为何大神偏爱`[...arr]`,而非`arr.push()`?
react.js
my一阁4 小时前
一文解决Chrome使用
前端·chrome