React 源码专栏之 render() 函数执行之后都经历了什么?

要使用 React 渲染 UI,您应该首先执行以下步骤:

  1. 使用 createRoot 创建根对象。

  2. 调用 root.render(ui) 函数。

如下代码所示:

jsx 复制代码
import * as React from "react";
import * as ReactDOM from "react-dom/client";

import reportWebVitals from "./reportWebVitals";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"), {
  unstable_concurrentUpdatesByDefault: true,
});

root.render(<App />);

reportWebVitals();
1;

Root 节点是 ReactDOM 树中的基本组成部分,对于每个 Root 节点,有 render() 方法使其渲染或更新,以及 unmount() 方法将该节点从 ReactDOM 树上卸载。

节点 render() 的渲染更新的具体步骤则是由 react-Reconciliation 来协调完成的。因此在 ReactDOM 中,只定义了一个 Root 节点在调用被 render() 时,需要作什么样的检查以及相应的错误处理与提示。随后再调用 react-Reconciliation 完成具体的渲染更新操作。

unmount() 则是将 Root 节点从 ReactDOM 树中卸载出去,同样需要作出相关的检查、更新节点状态,并最后在 ReactDOM 树中取消标记其为 Root 节点。

理解 render 函数

在 React 内部实现中,我们可以将 render 函数分为两个阶段:

  1. 渲染阶段

  2. 提交阶段

其中渲染阶段可以分为 beginWork 和 completeWork 两个阶段,而提交阶段对应着 commitWork。

渲染阶段

beginWork 是 Fiber 树的创建过程的开始阶段。在这个阶段,React 会遍历组件树,从根节点开始,逐层向下处理每个节点。这个阶段的主要任务是:

  1. 比较新旧 Fiber 树(即 Reconciliation),找出变化的部分。

    比较新旧 Fiber 树(即 Reconciliation)是 React 中虚拟 DOM 处理的关键阶段。Reconciliation 的主要任务是找出新旧虚拟 DOM 树的差异,并将这些差异最小化地更新到实际的 DOM 树中。这个过程发生在 beginWork 和 completeWork 阶段。

    Reconciliation 过程主要发生在 beginWork 阶段。具体步骤如下:

    1. 创建新 Fiber 对象:React 从根节点开始,遍历新的虚拟 DOM 树,为每个节点创建对应的 Fiber 对象。

    2. 比较新旧 Fiber 树:对于每个节点,React 比较新旧 Fiber 对象,以确定节点是否发生了变化。这一过程称为diffing

    3. 生成变更列表:在比较过程中,React 会生成一份变更列表,记录需要对实际 DOM 进行的插入、更新和删除操作。

  2. 为每个组件创建或更新 Fiber 对象。

  3. 根据组件的类型(函数组件、类组件、原生组件等)执行相应的渲染逻辑。

  4. 对于类组件,调用 render 方法获取子元素;对于函数组件,调用函数得到子元素。

  5. 为每个节点生成子 Fiber 对象,并将它们连接到当前 Fiber 树中。

completeWork 是 Fiber 树的创建过程的完成阶段。在这个阶段,React 会从叶子节点开始,逐层向上处理每个节点。这个阶段的主要任务是:

  1. 确定每个节点及其子节点的最终属性和状态。

  2. 为原生组件生成 DOM 节点。

  3. 将生成的 DOM 节点插入到正确的位置。

  4. 完成对节点的任何必要的后处理工作。

通过将更新过程分为多个小步骤(即 beginWork 和 completeWork),React 可以在每一帧渲染时处理一小部分更新,从而避免长时间的阻塞。

提交阶段

commitWork 是 Fiber 树更新过程的提交阶段。在这个阶段,React 会将已完成的 Fiber 树变更应用到实际的 DOM 树中。这个阶段的主要任务是:

  1. 将更新后的 DOM 节点插入、更新或删除,确保浏览器中的 DOM 树与最新的 React 状态同步。

  2. 触发组件的生命周期方法(如 componentDidMount、componentDidUpdate)。

  3. 执行任何需要的副作用(如使用 useEffect 注册的副作用)。

render()

render 是一个方法,它是 ReactDOMRoot 的原型方法,它的主要代码如下所示:

ts 复制代码
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
  function (children: ReactNodeList): void {
    debugger;
    const root = this._internalRoot;
    if (root === null) {
      throw new Error("Cannot update an unmounted root.");
    }
    updateContainer(children, root, null, null);
  };

在这个方法里面主要是将 _internalRoot 传递给了 updateContainer 进行函数调用。

updateContainer

updateContainer 是在 React 代码库中从多个地方调用的一个函数,你可能会想为什么它被称为 update(更新)而不是 render(渲染)或 mount(挂载)?这是因为 React 始终将树视为正在更新。React 可以知道树的哪一部分是第一次挂载,并会在每次执行必要的代码。

它的代码如下:

ts 复制代码
// src/react/packages/react-reconciler/src/ReactFiberReconciler.old.js
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function
): Lane {
  // 获取当前的rootFiber对象
  const current = container.current;

  // 获取程序运行到目前为止的时间,用于进行优先级排序
  const eventTime = requestEventTime();
  // 同步直接返回 `SyncLane` = 1。以后开启并发和异步等返回的值就不一样了,目前只有同步这个模式
  const lane = requestUpdateLane(current);

  if (enableSchedulingProfiler) {
    // 在Performance接口中标记--schedule-render-{当前lane}用于性能分析
    markRenderScheduled(lane);
  }

  // 获取当前节点和子节点的上下文
  const context = getContextForSubtree(parentComponent);

  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }

  // 创建一个 update 对象
  const update = createUpdate(eventTime, lane);

  // 记录update的载荷信息
  update.payload = { element };

  // 如果有回调信息,保存
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }
  // 将新建的update入队
  const root = enqueueUpdate(current, update, lane);

  if (root !== null) {
    scheduleUpdateOnFiber(root, current, lane, eventTime);

    entangleTransitions(root, current, lane);
  }
  return lane;
}

这个函数做了很多事情,在首次挂载树以及后续更新时都会使用它。从 root.render 调用时,最后两个参数传递为 null,这意味着它们未被使用。

这个函数接收的容器并不是你传递给 createRoot 的 DOM 元素。这个容器是 root._internalRoot,它是一个 FiberRootNode。

如果你还记得之前的文章,container.current 属性的类型是 FiberNode,这是我们目前应用程序中创建的唯一 Fiber,而 element 是 React 节点列表或者说组件树。

ts 复制代码
const current = container.current;

requestUpdateLane

下一步要做的事情就是请求当前 Fiber 更新车道(lane),用于确定更新的优先级。对于同步模式,返回 SyncLane(值为 1)。

js 复制代码
const lane = requestUpdateLane(current);

requestUpdateLane 函数如下代码所示:

ts 复制代码
export function requestUpdateLane(fiber: Fiber): Lane {
  const mode = fiber.mode;
  if ((mode & ConcurrentMode) === NoMode) {
    // concurrent 模式
    return (SyncLane: Lane);
  } else if (
    !deferRenderPhaseUpdateToNextBatch &&
    (executionContext & RenderContext) !== NoContext &&
    workInProgressRootRenderLanes !== NoLanes
  ) {

    /**
     * 当新的更新任务产生时,workInProgressRootRenderLanes不为空,则表示有任务正在执行
     * 那么则直接返回这个正在执行的任务的lane,那么当前新的任务则会和现有的任务进行一次批量更新
     */
    return pickArbitraryLane(workInProgressRootRenderLanes);
  }

  /**
   * 检查当前事件是否为过渡优先级,例如使用了 Suspense 或者 useTransition
   * 每次调用优先级都会降低
   * 过渡优先级共有16位:当所有位都使用完后,则又从第一位开始赋予事件过渡优先级
   */
  const isTransition = requestCurrentTransition() !== NoTransition;
  if (isTransition) {

    if (currentEventTransitionLane === NoLane) {
      currentEventTransitionLane = claimNextTransitionLane();
    }
    console.log('当前优先级',currentEventTransitionLane);
    return currentEventTransitionLane;
  }

  /**
   * 返回当前任务优先级
   * updateLane 优先级为 0
   */
  const updateLane: Lane = (getCurrentUpdatePriority(): any);
  if (updateLane !== NoLane) {
    return updateLane;
  }

  /**
   * 返回当前事件的优先级
   * 如果没有事件返回,则返回 DefaultEventPriority
   * 如果有,根据事件类型返回优先级
   * eventLane 优先级为16
   */
  const eventLane: Lane = (getCurrentEventPriority(): any);
  return eventLane;
}

该函数最终返回的 eventLane 的值为 16 表示 DefaultLane 也就是默认优先级。

这个函数的主要作用是根据当前的运行环境、模式和事件,确定并返回适当的优先级车道(Lane)。优先级的确定涉及到并发模式、当前正在执行的任务、过渡优先级和事件优先级等多个因素。

React 使用过渡优先级来处理一些非紧急的任务,例如使用 Suspense 或者 useTransition 触发的任务。这些任务通常不会立即影响用户体验,所以它们可以被推迟到浏览器空闲时执行。

React 的过渡优先级共有 16 个(用 16 位二进制表示),每次调用时,React 会选择一个未被使用的位作为当前任务的优先级。一旦所有 16 个优先级位都被使用过,React 会重新从第一位开始循环使用。这就意味着随着每次调用,分配给任务的优先级会逐渐降低,直到循环使用的位被重新分配。

createUpdate

接下来代码继续往下执行,它会使用 createUpdate 函数创建一个 update 对象

js 复制代码
const update = createUpdate(eventTime, lane);
update.payload = { element };

在 React 中,update 对象表示一次更新操作,包含 payload 属性用于存储更新的数据,这些 update 对象被添加到更新队列中,React 会遍历这个队列应用更新,从而生成新的组件状态。

如下代码所示:

js 复制代码
function createUpdate(eventTime, lane) {
  return {
    eventTime,
    lane,
    payload: null,
  };
}

const update = createUpdate(Date.now(), someLane);
update.payload = { element: <div>Moment</div> };

调用 createUpdate 函数创建了一个新的 update 对象。update.payload 被设置为 { element: <div>Moment</div> },这意味着这次更新将会把 element 更新为 <div>Moment</div>

enqueueUpdate

update 对象创建完成之后,接着是将新建的 update 入队:

js 复制代码
// 将新建的update入队
const root = enqueueUpdate(current, update, lane);

enqueueUpdate 函数如下定义:

js 复制代码
export function enqueueUpdate<State>(
  fiber: Fiber,
  update: Update<State>,
  lane: Lane
): FiberRoot | null {
  const updateQueue = fiber.updateQueue;
  if (updateQueue === null) {
    // fiber 被卸载时
    return null;
  }

  // 返回一个对象 {interleaved:null, lanes:0, pending:null}
  const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

  // pending 永远指向最后一个更新
  if (isUnsafeClassRenderPhaseUpdate(fiber)) {
    const pending = sharedQueue.pending;
    if (pending === null) {
      // 第一次更新创建一个循环单链表
      update.next = update;
    } else {
      // 如果更新队列不为空,取出第一个更新
      update.next = pending.next;
      pending.next = update;
    }
    sharedQueue.pending = update;
    return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
  } else {
    return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
  }
}

在 enqueueUpdate 函数中,首先从 fiber 对象中获取 updateQueue,如果 updateQueue 为空,说明该 fiber 已经被卸载,直接返回 null。

此时 updateQueue 里面的属性基本显示全为空的状态。

接下来是获取共享队列:

js 复制代码
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

从 updateQueue 中获取共享队列 sharedQueue。sharedQueue 是一个对象,包含三个属性:interleaved、lanes 和 pending。

此时代码进入到最后阶段:

js 复制代码
if (isUnsafeClassRenderPhaseUpdate(fiber)) {
  const pending = sharedQueue.pending;
  if (pending === null) {
    // 第一次更新创建一个循环单链表
    update.next = update;
  } else {
    // 如果更新队列不为空,取出第一个更新
    update.next = pending.next;
    pending.next = update;
  }
  sharedQueue.pending = update;
  return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
} else {
  return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
}
  1. 如果 fiber 处于不安全的类渲染阶段,进入这个分支。

  2. sharedQueue.pending 永远指向最后一个更新。

  3. 如果 pending 为空,说明这是第一个更新,需要创建一个循环单链表,将 update.next 指向 update 自己。

  4. 如果 pending 不为空,取出第一个更新并插入新的更新,使其成为循环单链表的一部分。

  5. 更新 sharedQueue.pending 指向新的 update。

  6. 调用 unsafe_markUpdateLaneFromFiberToRoot 标记更新从 fiber 到根 Fiber,并返回根 Fiber。

在初始的状态,sharedQueue.pending 为 null。第一次更新的时候 sharedQueue.pending 指向 update1,并且 update1.next 指向 update1 自己,形成一个循环链表。

第二次更新的时候 sharedQueue.pending 更新为指向 update2,update2.next 指向 update1,update1.next 指向 update2,形成新的循环链表。

enqueueConcurrentClassUpdate

这段代码定义了一个函数 enqueueConcurrentClassUpdate,用于将类组件的更新加入到并发更新队列中。

js 复制代码
export function enqueueConcurrentClassUpdate<State>(
  fiber: Fiber,
  queue: ClassQueue<State>,
  update: ClassUpdate<State>,
  lane: Lane
) {
  const interleaved = queue.interleaved;
  if (interleaved === null) {
    update.next = update;

    pushConcurrentUpdateQueue(queue);
  } else {
    update.next = interleaved.next;
    interleaved.next = update;
  }
  queue.interleaved = update;
  return markUpdateLaneFromFiberToRoot(fiber, lane);
}

interleaved 是指向当前队列中的交错更新链表。如果 interleaved 为 null,表示这是第一个更新。

  1. 将 update.next 指向自身,形成一个循环链表。

  2. 调用 pushConcurrentUpdateQueue(queue),将此队列的交错更新推到全局的并发更新队列中,以便在当前渲染结束时处理这些更新。

如果 interleaved 不是 null,则表示已经有更新存在队列中:

  1. 将新更新的 next 指向当前队列的下一个更新。

  2. 将当前更新的 next 设置为新更新,使其插入到链表中。

最后,将 queue.interleaved 指向新更新,使其成为链表中的最新节点。

你看,全都是一模一样的,那么为什么有了 update,还要 queue 呢?

为什么有了 update,还要 queue 呢

在 React 的更新机制中,update 和 queue 都是管理组件状态更新的关键概念,但它们的职责和作用有所不同:

  1. update 是一个表示单个状态更新的对象。它包含了具体的更新内容,比如新的状态值或更新函数。每次调用 setState 或 forceUpdate,都会创建一个新的 update 对象。

  2. queue 是一个状态更新队列,用于管理多个 update 对象。在类组件中,queue 通常包含一个链表,用于按顺序存储多个 update 对象。queue 负责协调和管理这些更新,确保它们按正确的顺序和时间被处理。

queue 可以将多个 update 对象链接起来,形成一个更新链表。这样可以一次性处理多个更新,提高性能。它负责协调这些更新的执行顺序,确保它们按正确的顺序被处理。例如,如果有多个 setState 调用,它们会被依次添加到 queue 中,并按顺序处理。

并且 queue 允许 React 在必要时进行批量更新。这意味着在某些情况下,多个状态更新可以被合并为一次更新,减少不必要的重新渲染。

假设我们有一个计数器组件,在按钮点击事件中,我们会多次更新状态,但是我们希望这些状态更新能够批量处理,只触发一次重新渲染。

jsx 复制代码
import React, { useState, useEffect } from "react";

const Counter = () => {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // 多次调用 setCount 更新状态
    setCount((prevCount) => {
      console.log("Update 1:", prevCount + 1);
      return prevCount + 1;
    });
    setCount((prevCount) => {
      console.log("Update 2:", prevCount + 1);
      return prevCount + 1;
    });
    setCount((prevCount) => {
      console.log("Update 3:", prevCount + 1);
      return prevCount + 1;
    });
  };

  useEffect(() => {
    console.log("Effect:", count);
  }, [count]);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
};

export default Counter;

最终结果如下图所示:

这就是我们经常所谈及的批处理,多次同时调用,它会在一个事件循环中将多次状态更新合并成一次更新,以提高性能。在上面的代码示例中,我们调用了三次 setCount,每次都增加 1。由于这些调用是在同一个事件处理函数中发生的,React 会将它们合并成一次更新。因此,count 将从 0 增加到 3,而不是依次更新每次增加 1。

所以 useEffect 这个 hook 只执行了一次。

这个时候我们的代码要回到 enqueueConcurrentClassUpdate 中了,它调用 markUpdateLaneFromFiberToRoot(fiber, lane) 来标记从当前 Fiber 节点到根节点的更新优先级,并返回根节点。这确保了在整个更新过程中,所有相关节点都标记了正确的优先级,从而能在适当的时机进行更新。

最后又退回到 enqueueUpdate 函数中,该函数最终返回 enqueueConcurrentClassUpdate 函数的返回,将更新任务添加到并发类组件的更新队列中,并返回包含此更新的 Fiber 树的根。

在后续的代码中:

js 复制代码
if (root !== null) {
  scheduleUpdateOnFiber(root, current, lane, eventTime);
  // 如果fiberRoot存在,则纠缠车道队列
  entangleTransitions(root, current, lane);
}

就暂时不讲了,因为这已经要开始进入到 schedule ��度了,将会在后面的内容中讲解。

总的来说,updateContainer 函数负责将新的 React 元素树添加到更新队列中,并根据当前的上下文和优先级进行调度。它首先获取当前的 rootFiber 对象和事件时间,然后创建一个新的 update 对象,将新的 React 元素树记录到 update 中,并将其入队到 fiber 节点的更新队列中。最后,调度更新任务,并纠缠车道队列以确保更新能够正确地进行。

总结

ReactDOMRoot.prototype.render 是 React 的核心方法之一,它负责将 React 元素渲染到 DOM 中。在 React 18 及更高版本中,使用 createRoot API 创建的根节点上调用该方法,以启动和更新 React 应用。其主要任务是协调渲染过程,并触发必要的更新逻辑。

updateContainer 是 render 方法中的关键步骤,它执行以下任务:

  1. 获取当前 Fiber 节点:从容器中获取当前的 rootFiber 对象。

  2. 确定优先级:使用 requestUpdateLane 确定当前更新的优先级。

  3. 标记性能分析:如果启用了调度分析器(Scheduling Profiler),则进行标记。

  4. 获取上下文:获取当前节点和子节点的上下文。

  5. 创建更新对象:创建一个新的更新对象,包含更新的数据(新的 React 元素树)。

  6. 将更新入队:将新创建的更新对象入队,确保它被正确处理。

  7. 调度更新:调用 scheduleUpdateOnFiber 函数调度更新任务。

ReactDOMRoot.prototype.render 是 React 渲染过程中的关键方法。它通过调用 updateContainer 函数,协调组件树的更新,并确保更新任务被正确调度。

相关推荐
xjt_09015 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农16 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king41 分钟前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发2 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法