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 函数,协调组件树的更新,并确保更新任务被正确调度。

相关推荐
Martin -Tang10 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发11 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
老码沉思录1 小时前
React Native 全栈开发实战班 - 第四部分:用户界面进阶之动画效果实现
react native·react.js·ui
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习