Skip to Content

面试导航 - 程序员面试题库大全 | 前端后端面试真题 | 面试

Reactrender

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

  1. 使用 createRoot 创建根对象。

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

如下代码所示:

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 的原型方法,它的主要代码如下所示:

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 可以知道树的哪一部分是第一次挂载,并会在每次执行必要的代码。

它的代码如下:

// 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 节点列表或者说组件树。

const current = container.current;

requestUpdateLane

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

const lane = requestUpdateLane(current);

requestUpdateLane 函数如下代码所示:

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 对象

const update = createUpdate(eventTime, lane); update.payload = { element };

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

如下代码所示:

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 入队:

// 将新建的update入队 const root = enqueueUpdate(current, update, lane);

enqueueUpdate 函数如下定义:

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 里面的属性基本显示全为空的状态。

接下来是获取共享队列:

const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

从 updateQueue 中获取共享队列 sharedQueue。sharedQueue 是一个对象,包含三个属性:interleaved、lanes 和 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); }
  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,用于将类组件的更新加入到并发更新队列中。

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 在必要时进行批量更新。这意味着在某些情况下,多个状态更新可以被合并为一次更新,减少不必要的重新渲染。

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

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 树的根。

在后续的代码中:

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

最后更新于:
Copyright © 2025Moment版权所有粤ICP备2025376666