Skip to content

React 原理篇

问题 1:虚拟 DOM 的意义

优点

  1. 减少实际的 DOM 操作(提高性能):通过比较新旧虚拟 DOM 树的差异,React 可以确定需要更新的部分,并生成最小化的 DOM 操作序列。这样可以减少实际的 DOM 操作次数,提高性能。
  2. 批量更新:React 会将所有需要更新的 DOM 操作批量执行,从而避免了频繁的 DOM 操作,提高了性能。
  3. 跨平台兼容性:虚拟 DOM 是一个轻量级的 JavaScript 对象,可以在不同的平台上运行,例如浏览器、移动设备和服务器。这使得 React 可以在多个环境中使用相同的代码和逻辑。
  4. 更好的开发体验(简化开发):虚拟 DOM 使得开发者可以使用类似于 HTML 的标记语言来描述 UI,而不需要直接操作 DOM。这简化了开发过程,并提供了更好的开发体验。

缺点

  1. 内存消耗:虽然虚拟 DOM 提高了渲染效率,但是它需要额外的内存空间来存储虚拟 DOM 树及其状态,对于大规模的应用(例如 3D 可视化等)或资源受限的设备来说,可能会成为一个负担。
  2. 初次渲染延迟:首次加载应用时,构建虚拟 DOM 树并将其转换为真实 DOM 的过程会增加一定的初始化时间,导致首屏加载速度变慢。
  3. 复杂性增加:对于一些简单的应用场景,引入虚拟 DOM 机制可能反而增加了不必要的复杂度。此外,理解虚拟 DOM 的工作原理以及如何高效地使用相关框架,也需要一定的学习成本。

问题 2:JSX

JSX 转为真实 DOM 的过程?

  • 虚拟 DOM:js 对象,用来描述 DOM 结构的

  • AST:js 对象,用来描述代码结构的

  • DSL(数据协议设计):js 对象,用来描述特定领域的业务数据(例如:低代码平台的 json schema,vue、react 的 vnode)

  1. jsx 代码
jsx
return (
  <div className='box'>
    {/* 组件 */}
    <Header>hello</Header>
    {/* 元素 */}
    <div>container</div>
    {/* 文本 */}
    footer
  </div>
)
  1. 使用 Babel 将 jsx 转为 React.createElement 后

React.createElement(type, props, ...children)

js
// 编译后(React 17 之前)
React.createElement(
  'div',
  { className: 'box' },
  React.createElement(Header, null, 'hello'),
  React.createElement('div', null, 'container'),
  'footer'
)
// 编译后(React 17+,自动引入 _jsx)
import { jsx as _jsx } from 'react/jsx-runtime'
const element = _jsx('h1', { className: 'title', children: 'Hello, world!' })
  1. 创建虚拟 DOM

React.createElement 返回一个描述 UI 的 JavaScript 对象(即虚拟 DOM)。

  1. 调用 ReactDOM.render

使用 ReactDOM.render 方法将虚拟 DOM 渲染到指定的挂载容器中。

js
ReactDOM.render(虚拟DOM, document.getElementById('root'))
  1. Diff 算法比较差异
  • React 使用高效的 Diff 算法来比较新旧虚拟 DOM 树之间的差异。
  • 只更新有变化的部分,生成最小化的 DOM 操作序列。
  1. 批量更新与优化

React 会将所有需要更新的 DOM 操作批量执行,以减少实际的 DOM 操作次数,提高性能

  1. 生成真实 DOM

最终,React 将这些最小化的 DOM 操作应用到浏览器的真实 DOM 中,完成页面的渲染或更新。

JSX 循环为何使用 key ?

  1. 元素的高效识别与复用

React 通过 key 唯一标识列表中的每个元素。当列表发生变化(增删改排序)时,React 会通过 key 快速判断:

  • 哪些元素是新增的(需要创建新 DOM 节点)
  • 哪些元素是移除的(需要销毁旧 DOM 节点)
  • 哪些元素是移动的(直接复用现有 DOM 节点,仅调整顺序)

如果没有 key,React 会默认使用数组索引(index)作为标识,这在动态列表中会导致 性能下降状态错误

  1. 避免状态混乱

如果列表项是 有状态的组件(比如输入框、勾选框等),错误的 key 会导致状态与错误的内容绑定。例如:

jsx
// 如果初始列表是 [A, B],用索引 index 作为 key:
<ul>
  {items.map((item, index) => (
    <li key={index}>{item}</li>
  ))}
</ul>

// 在头部插入新元素变为 [C, A, B] 时:
// React 会认为 key=0 → C(重新创建)
// key=1 → A(复用原 key=0 的 DOM,但状态可能残留)
// 此时,原本属于 A 的输入框状态可能会错误地出现在 C 中。
  1. 提升渲染性能

通过唯一且稳定的 key(如数据 ID),React 可以精准判断如何复用 DOM 节点。如果使用随机数或索引,每次渲染都会强制重新创建所有元素,导致性能浪费。

问题 3:React DOM 的 diff 算法

diff 算法

React 的虚拟 DOM diff 算法是一种用于比较新旧虚拟 DOM 树的差异的算法,目标是找出需要更新的部分,并生成一个最小化的 DOM 操作序列:

  1. 比较根节点:算法首先比较新旧虚拟 DOM 树的根节点。如果它们的类型不同,那么 React 会完全替换旧的 DOM 树。如果它们的类型相同,那么算法会继续比较它们的属性和子节点。
  2. 比较属性:算法会比较新旧虚拟 DOM 树的属性,判断是否有属性发生了变化。如果有属性发生了变化,React 会更新对应的 DOM 节点上的属性。
  3. 比较子节点:算法会递归地比较新旧虚拟 DOM 树的子节点。如果子节点的数量不同,那么 React 会更新对应的 DOM 节点的子节点。如果子节点的数量相同,那么算法会继续比较它们的类型和内容。
  4. 递归比较:算法会递归地比较新旧虚拟 DOM 树的子节点。如果子节点的类型相同,那么算法会继续比较它们的属性和子节点。如果子节点的类型不同,那么 React 会完全替换旧的 DOM 节点。
  5. 生成 DOM 操作序列:通过比较新旧虚拟 DOM 树,算法会生成一个最小化的 DOM 操作序列,包括插入、更新和删除操作。React 会将这些操作批量执行,从而减少实际的 DOM 操作次数。

react diff 和 vue diff 的区别

静态优化机制

  1. Vue 的编译时优化(静态节点标记):模板中的静态节点(无响应式绑定)会被编译为常量,跳过 diff,以及预字符串化。
vue 模版编译优化
  1. 虚拟节点静态标记(Patch Flag):更新类型标记,去标注要变化什么

    • Vue 3 引入了 Patch Flag,这是一种优化技术,它在编译时标记虚拟节点的动态部分。这样在组件更新时,Vue 只需要关注这些被标记的部分,而不是整个组件树,从而显著提升了性能。

    对于单个有动态绑定的元素来说,我们可以在编译时推断出大量信息:

    html
    <!-- 仅含 class 绑定 -->
    <div :class="{ active }"></div>
    <!-- 仅含 id 和 value 绑定 -->
    <input
      :id="id"
      :value="value"
    />
    <!-- 仅含文本子节点 -->
    <div>{{ dynamic }}</div>

    在为这些元素生成渲染函数时,Vue 在 vnode 创建调用中直接编码了每个元素所需的更新类型:

    js
    createElementVNode(
      'div',
      {
        class: _normalizeClass({ active: _ctx.active })
      },
      null,
      2 /* CLASS */
    )

    最后这个参数 2 就是一个更新类型标记 (patch flag)。一个元素可以有多个更新类型标记,会被合并成一个数字。运行时渲染器也将会使用位运算来检查这些标记,确定相应的更新操作:

    js
    if (vnode.patchFlag & PatchFlags.CLASS /* 2 */) {
      // 更新节点的 CSS class
    }

    内容在官网中有详细介绍:https://cn.vuejs.org/guide/extras/rendering-mechanism.html#patch-flags

  2. 缓存静态内容

html
<div>
  <!-- 需缓存 -->
  <div>foo</div>
  <!-- 需缓存 -->
  <div>bar</div>
  <div>{{ dynamic }}</div>
</div>

foobar 这两个 div 是完全静态的,会完全跳过对它们的差异比对。

  1. 预字符串化:当有足够多连续的静态元素时,它们还会再被压缩为一个“静态 vnode”,其中包含的是这些节点相应的纯 HTML 字符串。

会把静态内容提前转换成字符串。

html
<div>
  <div class="foo">foo</div>
  <div class="foo">foo</div>
  <div class="foo">foo</div>
  <div class="foo">foo</div>
  <div class="foo">foo</div>
  <div>{{ dynamic }}</div>
</div>
js
import {
  createElementVNode as _createElementVNode,
  toDisplayString as _toDisplayString,
  createStaticVNode as _createStaticVNode,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock
} from 'vue'

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock('div', null, [
      _cache[0] ||
        (_cache[0] = _createStaticVNode(
          '<div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div><div class="foo">foo</div>',
          5
        )),
      _createElementVNode(
        'div',
        null,
        _toDisplayString(_ctx.dynamic),
        1 /* TEXT */
      )
    ])
  )
}
  1. React 的运行时优化(手动控制更新): 需通过 React.memo、shouldComponentUpdate 或 useMemo 避免无效渲染

优化的设计方向:react 是运行时优化(Fiber 调度),vue 是编译时优化(模板静态分析)

问题 4:Fiber 架构

Fiber 是什么

Fiber 是 React 中一种新的架构,它用于实现增量式的、可中断的虚拟 DOM diff 过程。Fiber 的目标是改进 React 的性能和用户体验,使得 React 应用程序更加流畅和响应。

在 React 的旧版本中,虚拟 DOM diff 过程是一个递归的过程,它会一直执行直到完成,期间无法中断。这可能会导致长时间的 JavaScript 执行,从而阻塞主线程,造成页面的卡顿和不流畅的用户体验。

为了解决这个问题,React 引入了 Fiber 架构。Fiber 将整个虚拟 DOM diff 过程分为多个小任务,每个任务称为一个 Fiber 节点。这些 Fiber 节点被组织成一个树状结构,称为 Fiber 树。

Fiber 的主要优势在于:

  • 可中断的渲染:Fiber 树可以被中断和恢复,这意味着在执行 Fiber 树的 diff 过程时,可以在任意时刻中断当前任务,并优先执行其他任务。Fiber 允许将大的渲染任务拆分成多个小的工作单元(Unit of Work),使得 React 可以在空闲时间执行这些小任务。当浏览器需要处理更高优先级的任务时(如用户输入、动画),可以暂停渲染,先处理这些任务,然后再恢复未完成的渲染工作。

通过检索 flagssubtreeFlags 来检测该 fiber 是不是渲染完成了。

  • flags 字段用于标记当前 Fiber 节点的副作用(side effects),例如插入、删除、更新等。

  • subtreeFlags 字段用于标记当前 Fiber 节点及其子树中的副作用。

如果一个 Fiber 节点的 flags 和 subtreeFlags 字段都没有设置副作用标记,说明该节点及其子树已经渲染完毕。

flags 和 subtreeFlags 的具体作用,以及如何判断渲染完成的

要更具体地理解,我们可以结合 React Fiber 的源码逻辑和示例来分析:

一、先明确「副作用(Side Effects)」的类型

在 React 中,flags(在源码中也叫 EffectTag)有很多枚举值,比如:

  • NoFlags:无标记,表示该节点没有需要处理的副作用。
  • Placement:需要将节点插入到 DOM。
  • Update:需要更新节点的属性、内容。
  • Deletion:需要从 DOM 中删除节点。
  • ChildDeletion:子节点需要被删除。
  • 还有 Passive(用于处理 useEffect 等副作用)等。

二、flagssubtreeFlags 在 Fiber 节点中的结构

Fiber 节点的简化结构类似这样:

javascript
function FiberNode(tag, pendingProps, key) {
  // 其他属性...
  this.flags = NoFlags // 初始为“无标记”
  this.subtreeFlags = NoFlags // 初始为“无标记”
  this.child = null // 子 Fiber 节点
  this.sibling = null // 兄弟 Fiber 节点
  // ...
}

// 枚举所有副作用类型(简化版)
const NoFlags = 0b00000
const Placement = 0b00001
const Update = 0b00010
const Deletion = 0b00100
// ...更多类型

三、如何通过 flagssubtreeFlags 检测渲染完成?

当 React 处理 Fiber 树时,会递归地收集“副作用标记”:

  1. 给 Fiber 节点设置 flags

比如一个组件需要更新内容,它的 Fiber 节点会被标记 Update

javascript
function markUpdate(fiber) {
  fiber.flags |= Update // 按位或,设置 Update 标记
}
  1. 给子树设置 subtreeFlags

如果一个父节点的子树中有节点需要插入(Placement),父节点的 subtreeFlags 会被标记:

javascript
function markSubtree(fiber) {
  // 递归遍历子树,收集所有副作用
  if (fiber.child) {
    markSubtree(fiber.child)
    fiber.subtreeFlags |= fiber.child.subtreeFlags
  }
  if (fiber.sibling) {
    markSubtree(fiber.sibling)
    fiber.subtreeFlags |= fiber.sibling.subtreeFlags
  }
  // 同时合并自身的 flags 到 subtreeFlags
  fiber.subtreeFlags |= fiber.flags
}
  1. 判断是否渲染完成

当一个 Fiber 节点的 flagssubtreeFlags 都为 NoFlags 时,说明它自身和子树都没有需要执行的副作用,即渲染完成

javascript
function isFiberRendered(fiber) {
  return fiber.flags === NoFlags && fiber.subtreeFlags === NoFlags
}

四、实际流程示例

假设我们有一个 Fiber 树结构:

  • 根节点 A,子节点 BB 的子节点 C
  • C 需要执行「更新」操作(flags = Update)。

执行 markSubtree(A) 后:

  • C.flags = Update
  • B.subtreeFlags = Update(因为子节点 C 有副作用)
  • A.subtreeFlags = Update(因为子节点 B 的子树有副作用)

此时 isFiberRendered(A) 会返回 false(因为 A.subtreeFlags !== NoFlags)。

C 的更新操作执行完毕后,会清除 C.flags 并重新收集子树标记:

  • C.flags = NoFlags
  • B.subtreeFlags = NoFlags(子树无副作用)
  • A.subtreeFlags = NoFlags(子树无副作用)

此时 isFiberRendered(A) 返回 true,说明整个树渲染完成。

通过这种机制,React 能精准地知道“哪些节点还需要处理副作用”,从而高效地调度渲染流程~

  • 优先级调度:React 可以根据任务的优先级动态地调整任务的执行顺序,会优先更新用户可感知的部分(如动画、用户输入),而低优先级的任务(如数据加载后的界面更新)可以延后执行,从而更好地控制 JavaScript 的执行。

React 为不同任务分配优先级(如用户输入 > 动画 > 普通更新),通过调度器(Scheduler)实现任务的中断与切换,高优先级可以打断正在执行的低优先级任务。

调度器:负责管理任务的优先级,确保高优先级的任务能够及时执行。

  • 双缓存树优化 Diff (Fiber Tree):Fiber 架构中有两棵 Fiber 树——current fiber tree(当前正在渲染的 Fiber 树)和 work in progress fiber tree(正在处理的 Fiber 新树),在新树构建完成后一次性替换旧树(curent fiber)。React 使用这两棵树来保存更新前后的状态,从而更高效地进行比较和更新,保证渲染到屏幕的始终是完整的 UI。
双树的作用

React 采用双 Fiber 树(current 树和 work in progress 树)来保存更新前后状态,主要是为了实现渲染的原子性可中断性

  1. 保证渲染的原子性

通过先在 work in progress 树中完成所有更新计算,再一次性替换 current 树,能确保用户看到的始终是完整、一致的 UI,避免“半成品”渲染

  1. 支持可中断的渲染调度

双树结构让 React 可以:

  • 在构建 work in progress 树时,若有更高优先级任务(如用户点击、动画),随时中断当前构建过程,优先处理高优先级任务
  • 后续恢复时,从 work in progress 树的 “断点” 继续构建,无需从头开始,大幅提升渲染效率。

总结

  • work in progress 树是 React 在计算更新阶段构建的 “临时树”,它承载了最新的状态、属性和 DOM 变更。React 会在这棵树上执行 Diff 算法、计算副作用(如哪些节点需要插入、更新、删除),并完成所有 “逻辑层面的更新计算”。
  • current 树是当前已经渲染到屏幕上的 “真实” 树,它反映了用户当前看到的 UI 状态。只有当 WIP 树完全构建好(所有更新逻辑计算完毕),React 才会一次性替换 current 树,让新的 UI 原子性地渲染到屏幕上。

简单来说,WIP 树是 “草稿纸”(用来计算更新),current 树是 “最终作品”(用来渲染到屏幕)。

两树在 react fiber 更新的工作阶段
  • Work In Progress(WIP)树:工作在 协调阶段(Reconciliation),核心是计算更新(Diff 对比、生成新 Fiber 节点、标记副作用)。
  • current 树:在协调阶段作为“旧状态参考”,最终在 提交阶段(Commit) 被 WIP 树替换,新 UI 渲染到屏幕。
  • 任务切片:在浏览器的空闲时间内(利用 requestIdleCallback 思想),React 可以将渲染任务拆分成多个小片段,逐步完成 Fiber 树的构建,避免一次性完成所有渲染任务导致的阻塞。

总而言之,Fiber 是 React 中一种新的架构,用于实现增量式的、可中断的虚拟 DOM diff 过程。它通过将 diff 过程分为多个小任务,并根据优先级动态地调整任务的执行顺序,提高 React 应用程序的性能和响应性。

Fiber 节点

每个组件对应一个 Fiber 节点,构成双向链表树结构,包含以下关键信息:

  • 组件类型:函数组件、类组件或原生标签。
  • 状态与副作用:Hooks 状态(如 useState)、生命周期标记(如 useEffect)。
  • 调度信息:任务优先级(lane 模型)、到期时间(expirationTime)。
  • 链表指针child(子节点)、sibling(兄弟节点)、return(父节点)。
js
// Fiber 节点结构简化示例
const fiberNode = {
  tag: FunctionComponent, // 组件类型
  stateNode: ComponentFunc, // 组件实例或 DOM 节点
  memoizedState: {
    /* Hooks 链表 */
  },
  pendingProps: {
    /* 待处理 props */
  },
  lanes: Lanes.HighPriority, // 任务优先级
  child: nextFiber, // 子节点
  sibling: null, // 兄弟节点
  return: parentFiber // 父节点
}

React Fiber 更新过程分为两个阶段:

  • Reconciliation(协调阶段,也叫调度阶段):此阶段会进行 Fiber 树的构建(包括新 Fiber 树的创建和与旧 Fiber 树的对比,双树对比,即协调过程),同时标记出需要执行的副作用(如 DOM 操作、生命周期方法调用等)。由于该阶段的操作不直接涉及 DOM 修改,这个阶段是可以被中断的。

  • Commit 阶段:阶段的主要任务是将 Reconciliation 阶段标记的副作用应用到实际 DOM 上,包括执行 DOM 增删改操作、调用相关生命周期方法等,这个阶段必须一次性完成,不能中断。

Fiber 如何实现可中断更新?

将渲染工作拆分为小单元(Fiber 节点),通过链表结构存储 Fiber 节点,记录遍历进度。每次处理一个 Fiber 后检查剩余时间片,不足时保存当前进度,将控制权交还浏览器。

具体实现原理
  1. Fiber 数据结构:工作单元的拆分

Fiber 本质上是对 React 元素的重新封装,每个 Fiber 节点对应一个组件或 DOM 元素,同时包含以下关键信息:

  • child/return/sibling:构建 Fiber 树的链表结构(替代传统栈递归),支持“指针跳转”式遍历,而非一次性递归到底。
  • pendingProps/memoizedProps:用于对比前后属性变化。
  • effectTag:标记当前节点需要执行的操作(如插入、更新、删除)。
  • alternate:指向“双缓存”中的另一棵 Fiber 树(current 树与 workInProgress 树)。

这种链表结构允许 React 随时暂停当前节点的处理,记录当前遍历位置(通过指针),后续可从该位置恢复。

  1. 链表结构:记录遍历进度

Fiber 节点通过 child(子节点)、sibling(兄弟节点)、return(父节点)形成链表结构,可以随时记录 “当前处理到哪个节点”。比如处理完一个 Fiber 节点的子节点后,能通过 sibling 找到下一个兄弟节点继续处理,也能通过 return 回到父节点,从而实现 “中断后能恢复” 的遍历逻辑。

  1. 时间切片:控制执行时长

React 利用浏览器的 requestIdleCallback 思想(实际使用自定义调度器 scheduler),将每段工作限制在一个“时间切片”(通常约 5ms)内:

  • 每次处理一个 Fiber 节点后,检查是否已超过当前时间切片。
  • 若超时,将控制权交还给浏览器(处理用户输入、渲染等紧急任务),并在下次空闲时恢复工作。

核心代码逻辑类似:

js
function workLoop(hasTimeRemaining) {
  let currentFiber = nextUnitOfWork
  while (currentFiber && hasTimeRemaining()) {
    // 处理当前 Fiber 节点
    currentFiber = performUnitOfWork(currentFiber)
    // 检查剩余时间
    hasTimeRemaining = checkTimeRemaining()
  }
  // 保存当前进度,下次恢复
  nextUnitOfWork = currentFiber
}
  1. 优先级调度:决定任务中断与恢复

React 为不同任务分配优先级(如用户输入 > 动画 > 普通更新),通过调度器(Scheduler)实现任务的中断与切换:

  • 高优先级任务(如点击事件)可以打断正在执行的低优先级任务。
  • 被打断的任务不会被丢弃,而是保存在 workInProgress 树中,待高优先级任务完成后,低优先级任务可从断点继续执行(或基于最新状态重新开始)。

优先级通过过期时间(expiration time)管理:优先级越高,过期时间越近,越先被执行。

  1. 双缓存机制:确保状态一致性

React 维护两棵 Fiber 树:

  • current 树:对应当前已渲染到 DOM 的状态。
  • workInProgress 树:正在构建的新树,所有更新操作在这棵树上进行。

当工作被中断时,workInProgress 树保存当前进度;当整棵树构建完成后,通过切换 current 指针指向新树,实现 DOM 的批量更新,避免中间状态的渲染。

Fiber 中断后恢复,如何知道节点处理没有呢,以及从哪个节点开始恢复呢?

要判断 Fiber 节点是否处理完成,React 是通过 Fiber 节点的 “完成标记” 和遍历流程的状态记录 来实现的,具体逻辑如下:

  1. 基于「完成标记」的直接判断

在 Fiber 节点的处理过程中,React 会通过 flagssubtreeFlags 字段 标记节点是否还有未完成的副作用(如 DOM 操作、状态更新等)。如果一个 Fiber 节点的 flags(自身副作用)和 subtreeFlags(子树副作用)都为 “无标记”(值为NoFlags),则说明该节点及其子树已经处理完成。

code
js
// Fiber 节点结构(简化)
class FiberNode {
  constructor() {
    this.flags = 0 // 自身副作用标记(如更新、插入)
    this.subtreeFlags = 0 // 子树副作用标记
    this.child = null // 子节点
    this.sibling = null // 兄弟节点
    this.return = null // 父节点
    this.effectTag = null // 额外的完成标记(可选)
  }
}

// 判断节点是否处理完成
function isFiberCompleted(fiber) {
  return fiber.flags === 0 && fiber.subtreeFlags === 0
}
  1. 基于「遍历流程」的进度记录

Fiber 的链表结构(childsiblingreturn)本身就是 “遍历进度” 的记录器。React 在处理 Fiber 树时,会通过 “当前处理节点的指针” 来跟踪进度,即使中途中断,也能从 “断点” 继续

  • 首先从根节点开始遍历
code
js
let currentFiber = rootFiber // 根节点
while (currentFiber) {
  // 处理当前 Fiber 节点(如计算 Diff、标记副作用)
  processFiber(currentFiber)

  // 优先处理子节点
  if (currentFiber.child) {
    currentFiber = currentFiber.child
  } else {
    // 没有子节点,处理兄弟节点
    while (currentFiber && !currentFiber.sibling) {
      // 若当前节点和兄弟节点都处理完,回到父节点
      currentFiber = currentFiber.return
    }
    if (currentFiber) {
      currentFiber = currentFiber.sibling
    }
  }
}
  • 中断与恢复的关键 ——“保存当前 Fiber 指针”

如果在遍历过程中需要中断(如时间片用尽),React 会保存当前的 currentFiber 指针。等后续恢复时,直接从这个指针继续遍历即可,无需从头开始。(可以看上面的代码)

问题 5:JSX 是怎么一步步转化成 Fiber 的?

JSX其实不是浏览器能识别的语法,本质是一个语法糖,最终被 Babel 编译为React.createElement调用,这个调用返回的是一个ReactElement的普通js对象,不设计真实渲染。真正的渲染是react根据它来构建fiber树。

fiber是react设计的一套数据结构,用来表示组件状态和更新信息。每个fiber节点都包含了当前组件的类型、props状态和父子关系、更新优先级等。

构建fiber树的过程发生在render阶段,react会遍历每个组件,执行beginWorkcompleteWork两个流程,边遍历边构建新的fiber树,这个过程最大的特点就是:可中断的。也就是说,渲染任务中有优先级更高的任务,例如用户点击了按钮,react可以暂停当前的渲染,优先处理高优先级任务,就避免了页面“一卡一卡”的问题。

等fiber树构建完成后,会进入commit阶段,会把已经标记、好变化的节点一次性更新到真实dom上,包括插入、更新、删除等操作。

所以整个流程分为:jsx编译成ReactElement,react根据ReactElement构建fiber树,fiber架构让渲染变得可中断、可调度,最后进入commit阶段统一更新dom。

问题 6:React concurrency 并发机制

什么是 React 的并发机制?

React 的并发机制(Concurrency)是 React 18 引入的一项重要特性,旨在提升应用的响应性和性能。

允许 React 在渲染过程中根据任务的优先级进行调度和中断,从而确保高优先级的更新能够及时渲染,而不会被低优先级的任务阻塞。

并发机制的工作原理

  • 时间分片(Time Slicing): React 将渲染任务拆分为多个小片段,每个片段在主线程空闲时执行。这使得浏览器可以在渲染过程中处理用户输入和其他高优先级任务,避免长时间的渲染阻塞用户交互。

  • 优先级调度(Priority Scheduling): React 为不同的更新分配不同的优先级。高优先级的更新(如用户输入)会被优先处理,而低优先级的更新(如数据预加载)可以在空闲时处理。

  • 可中断渲染(Interruptible Rendering): 在并发模式下,React 可以中断当前的渲染任务,处理更高优先级的任务,然后再恢复之前的渲染。这确保了应用在长时间渲染过程中仍能保持响应性。

问题 7:React reconciliation 协调的过程

React 的 协调(Reconciliation) 是用于高效更新 UI 的核心算法。当组件状态或属性变化时,React 会通过对比新旧虚拟 DOM(Virtual DOM)树,找出最小化的差异并应用更新。以下是协调过程的详细步骤:

  1. 生成虚拟 DOM 树:当组件状态或属性变化时,React 会重新调用组件的 render 方法,生成新的虚拟 DOM 树

  2. Diffing 算法(差异对比):React 使用 Diffing 算法 比较新旧两棵虚拟 DOM 树,找出需要更新的部分。

  3. 更新真实 DOM:通过 Diffing 算法找出差异后,React 将生成一系列最小化的 DOM 操作指令(例如 updateTextContentreplaceChild)。这些指令会被批量应用到真实 DOM 上,以减少重绘和重排的次数,提高性能。

  4. 协调的优化策略

    • Key 的作用:为列表元素提供唯一的 key,帮助 React 识别元素的移动、添加或删除,避免不必要的重建。
    • 批量更新(Batching):React 会将多个状态更新合并为一次渲染,减少重复计算。
    • Fiber 架构(React 16+):将协调过程拆分为可中断的“工作单元”(Fiber 节点),允许高优先级任务(如动画)优先处理。支持异步渲染(Concurrent Mode),避免长时间阻塞主线程。

问题 8:React 组件渲染和更新的全过程

React 组件的渲染和更新过程涉及多个阶段,包括 初始化、渲染、协调、提交、清理 等。

  1. 初始化阶段:创建 Fiber 树和 Hooks 链表。
  2. 渲染阶段:生成新的虚拟 DOM(Fiber 树)。
  3. 协调阶段:对比新旧 Fiber 树,找出需要更新的部分。
  4. 提交阶段:将更新应用到真实 DOM。
  5. 清理阶段:重置全局变量,准备下一次更新。
详细流程分析(要背)

(1)初始化阶段

  • 触发条件:组件首次渲染或状态/属性更新。
  • 关键函数rendercreateRootscheduleUpdateOnFiber
  • 逻辑
    1. 通过 ReactDOM.rendercreateRoot 初始化应用。
    2. 创建根 Fiber 节点(HostRoot)。
    3. 调用 scheduleUpdateOnFiber,将更新任务加入调度队列。

(2)渲染阶段

  • 触发条件:调度器开始执行任务。
  • 关键函数performSyncWorkOnRootbeginWorkrenderWithHooks
  • 逻辑
    1. 调用 performSyncWorkOnRoot,开始渲染任务。
    2. 调用 beginWork,递归处理 Fiber 节点。
    3. 函数组件调用 renderWithHooks,执行组件函数并生成新的 Hooks 链表。
    4. 对于 Host 组件(如 div),生成对应的 DOM 节点。

(3)协调阶段

  • 触发条件:新的虚拟 DOM 生成后。
  • 关键函数reconcileChildrendiff
  • 逻辑
    1. 调用 reconcileChildren,对比新旧 Fiber 节点。
    2. 根据 diff 算法,找出需要更新的节点。
    3. 为需要更新的节点打上 PlacementUpdateDeletion 等标记。

(4)提交阶段

  • 触发条件:协调阶段完成后。
  • 关键函数commitRootcommitWork
  • 逻辑
    1. 调用 commitRoot,开始提交更新。
    2. 调用 commitWork,递归处理 Fiber 节点。
    3. 根据节点的标记,执行 DOM 操作(如插入、更新、删除)。
    4. 调用生命周期钩子(如 componentDidMountcomponentDidUpdate)。

(5)清理阶段

  • 触发条件:提交阶段完成后。
  • 关键函数resetHooksresetContext
  • 逻辑
    1. 重置全局变量(如 currentlyRenderingFibercurrentHook)。
    2. 清理上下文和副作用。
    3. 准备下一次更新。

问题 9:为什么 Hook 调用顺序必须一致?

  • React 依赖于 Hook 的调用顺序来正确关联状态
  • 在每次渲染时,Hook 的调用顺序必须完全相同
  • 如果 Hook 调用顺序改变(比如由于条件语句),React 就无法正确跟踪状态

因为在 React 中,Hook 调用顺序必须保持一致是由其内部实现机制决定的,主要原因有以下几点:

  1. React 对 Hook 的管理方式: React 内部通过一个链表来跟踪组件中所有 Hook 的状态。每个 Hook 调用(如 useStateuseEffect)都会对应链表中的一个节点,记录着该 Hook 的状态信息。

  2. 依赖调用顺序识别 Hook: React 并不能通过 Hook 的名称或参数来识别不同的 Hook,而是完全依赖于调用顺序来匹配对应的状态。例如:

    jsx
    function Component() {
      const [name, setName] = useState('') // 第1个Hook
      const [age, setAge] = useState(0) // 第2个Hook
    
      useEffect(() => {
        /* ... */
      }, []) // 第3个Hook
      // ...
    }

    React 会默认认为每次渲染时,第 1 个 useState 始终对应 "name" 状态,第 2 个 useState 始终对应 "age" 状态。

  3. 条件判断会破坏调用顺序: 如果在条件语句(如 if)、循环或嵌套函数中调用 Hook,会导致每次渲染时 Hook 的调用顺序不一致。例如:

    jsx
    function Component() {
      if (someCondition) {
        const [name, setName] = useState('') // 可能不会被调用
      }
      const [age, setAge] = useState(0) // 调用顺序可能变化
      // ...
    }

    someConditionfalse 时,age 状态会被 React 误认为是第一个 Hook,导致状态匹配错误,进而引发难以预测的 Bug。

  4. 保证状态一致性: 一致的调用顺序确保了 React 能够在多次渲染之间正确关联 Hook 与其对应的状态,这是 React 实现 Hook 机制的基础。

因此,React 官方强制要求:Hook 必须在函数组件的顶层调用,不能在条件、循环或嵌套函数中使用,以保证每次渲染时 Hook 的调用顺序完全一致。

问题 10:Hooks 的链表结构

React 内部通过单向链表结构来管理组件中的 Hooks,这个链表是 Hooks 能够在多次渲染之间保持状态的核心机制。我们可以从以下几个方面理解这个结构:

1. 链表的基本构成

每个 Hook 对应链表中的一个节点,节点中包含以下关键信息:

  • memoizedState:存储当前 Hook 的状态(如 useState 的值、useEffect 的依赖项等)
  • next:指向链表中的下一个 Hook 节点(形成单向链表)
  • 其他元数据:如 useEffect 的回调函数、清理函数等

2. 链表的创建与更新过程

当组件首次渲染时:

  • React 会初始化一个 workInProgressHook 指针,指向当前正在处理的 Hook 节点
  • 每调用一个 Hook(如 useState),React 就会创建一个新的节点,将其添加到链表尾部
  • 节点的 memoizedState 会存储初始状态,next 指针指向下一个新创建的节点

当组件重新渲染时:

  • React 会重置 workInProgressHook 指针,使其指向链表的头部
  • 相同的顺序再次调用 Hooks 时,指针会依次移动到下一个节点,复用之前存储的 memoizedState

3. 简化的工作流程示例

假设组件中有两个 useState

jsx
function MyComponent() {
  const [count, setCount] = useState(0);  // Hook 1
  const [name, setName] = useState('');   // Hook 2
  return ...
}
  • 首次渲染

    1. 调用 useState(0) → 创建节点 1(memoizedState=0next=null
    2. 调用 useState('') → 创建节点 2(memoizedState=''next=null),并将节点 1 的 next 指向节点 2
    3. 形成链表:节点1 → 节点2
  • 重新渲染

    1. 再次调用第一个 useState → 指针指向节点 1,读取其 memoizedState(0)
    2. 再次调用第二个 useState → 指针移动到节点 2,读取其 memoizedState('')
    3. 保持链表结构不变,状态正确复用

4. 为什么顺序不能乱?

链表顺序依赖:Hooks 的存储依赖调用顺序,条件语句会破坏链表结构

如果某次渲染时 Hook 调用顺序改变(比如在条件判断中调用):

jsx
function MyComponent() {
  if (someCondition) {
    const [count, setCount] = useState(0) // 可能不执行
  }
  const [name, setName] = useState('') // 顺序错乱
}

someConditionfalse 时,第一个 useState 不执行,重新渲染时,React 会将第二个 useState 误认为是第一个 Hook,去读取节点 1 的状态(原本属于 count 的值),导致状态匹配错误,出现数据错乱或异常

5. 为什么只能在函数组件中使用?

因为 Hooks 需要绑定当前组件的 Fiber 节点,依赖函数组件的执行上下文,而普通函数无此上下文。

总结

Hooks 的链表结构依赖调用顺序来关联状态,每次渲染时通过指针依次访问节点。这种设计让 React 能够高效地管理多个 Hook 的状态,而无需额外的标识(如变量名)。这也是为什么 React 严格要求 Hooks 必须在组件顶层调用,不能放在条件、循环等可能改变执行顺序的代码中。

问题 11:React 的 hooks 的原理是怎样的

React Hooks 的原理可以从以下几个核心角度来理解:

  1. Hooks 依赖于调用顺序

    React Hooks 必须在函数组件的顶层调用,不能在条件语句、循环或嵌套函数中使用。这是因为 React 内部通过一个单向链表来管理 Hooks,每次调用 useStateuseEffect 时,都会按顺序将其添加到链表中。

    当组件重新渲染时,React 会按相同的顺序读取链表中的 Hooks 信息,从而保证状态的正确性。如果调用顺序改变,会导致 React 无法正确匹配 Hooks 与对应的状态。

  2. 状态存储机制

    • 每个函数组件都对应一个 Fiber 节点(React 内部工作单元)
    • Fiber 节点中包含一个 memoizedState 属性,用于存储该组件的 Hooks 链表
    • useState 会创建一个包含状态值和更新函数的对象,并将其添加到链表中
  3. 闭包的作用

    Hooks 严重依赖 JavaScript 的闭包特性。每次组件渲染时,都会创建新的函数作用域,而 Hooks 内部通过闭包保留对当前状态和 props 的引用。

    例如,useEffect 的回调函数会捕获定义它时的状态和 props,这就是为什么当依赖项变化时需要重新运行 effect。

  4. 依赖项数组的工作原理

    useEffectuseCallback 等 Hooks 接受一个依赖项数组作为参数。React 会在每次渲染时比较依赖项数组中的值:

    • 如果所有值都与上一次渲染相同,则跳过执行
    • 如果有任何值发生变化,则执行相应的回调函数

    这种机制让 React 能够优化性能,避免不必要的计算和副作用。

  5. Hooks 的本质

    Hooks 本质上是对函数组件内部状态和生命周期的抽象封装。它们允许在不编写类组件的情况下使用状态和其他 React 特性,同时保持代码的可组合性和可重用性。

简单来说,React 通过维护 Hooks 调用顺序和利用闭包特性,让函数组件能够拥有状态和副作用处理能力,同时保持了简洁的 API 和良好的性能。

问题 12:useEffect 的底层是如何实现的

useEffect 是 React 用于管理副作用的 Hook,它在 commit 阶段 统一执行,确保副作用不会影响渲染。

React 组件是通过 Fiber 数据结构 组织的,每个 useEffect 都会存储在 fiber.updateQueue 中。

useEffect 在 React 组件更新后,React 在 commit 阶段 统一遍历 effect 队列,并执行 useEffect 副作用。