Skip to content
返回

React 原理

目录

点击展开

React 原理

国内面试,大厂必考原理。

::: tip

  1. 目标不在中大厂的同学,可以略过这一节。
  2. 对 React 使用尚不熟练的同学,不要在此花费太多精力,先熟悉使用再说。

:::

JSX 的本质是什么?

参考答案

::: details

JSX(JavaScript XML) 是一个 JavaScript 的语法扩展,允许在 JavaScript 代码中通过类 HTML 语法创建 React 元素。它需要通过 Babel 等工具编译为标准的 JavaScript 代码,最终生成 React 元素对象(React Element),这些元素共同构成虚拟 DOM(Virtual DOM)树。

核心原理

  1. JSX 编译为 React 元素 JSX 会被转换为 React.createElement() 调用(或 React 17+ 的 _jsx 函数),生成描述 UI 结构的对象(React 元素),而非直接操作真实 DOM。

    // JSX
    const element = <h1 className="title">Hello, world!</h1>
    
    // 编译后(React 17 之前)
    const element = React.createElement('h1', { className: 'title' }, 'Hello, world!')
    
    // 编译后(React 17+,自动引入 _jsx)
    import { jsx as _jsx } from 'react/jsx-runtime'
    const element = _jsx('h1', { className: 'title', children: 'Hello, world!' })
  2. 虚拟 DOM 的运作

    • React 元素组成虚拟 DOM 树,通过 Diff 算法对比新旧树差异,最终高效更新真实 DOM。
    • 虚拟 DOM 是内存中的轻量对象,避免频繁操作真实 DOM 的性能损耗。

JSX 的核心特性

  1. 类 HTML 语法与 JavaScript 的融合

    • 表达式嵌入:通过 {} 嵌入 JavaScript 表达式(如变量、函数调用、三元运算符):

      const userName = 'Alice'
      const element = <p>Hello, {userName.toUpperCase()}</p>
    • 禁止语句{} 内不支持 if/for 等语句,需改用表达式(如三元运算符或逻辑与):

      <div>{isLoggedIn ? 'Welcome' : 'Please Login'}</div>
  2. 语法规则

    • 属性命名:使用驼峰命名(如 className 代替 classhtmlFor 代替 for)。
    • 闭合标签:所有标签必须显式闭合(如 <img />)。
    • 单一根元素:JSX 必须有唯一根元素(或用 <></> 空标签包裹)。
  3. 安全性

    • 默认 XSS 防护:JSX 自动转义嵌入内容中的特殊字符(如 < 转为 &lt;)。

    • 例外场景:如需渲染原始 HTML,需显式使用 dangerouslySetInnerHTML(需谨慎):

      <div dangerouslySetInnerHTML={{ __html: userContent }} />

编译与工具链

  1. 编译流程 JSX 需通过 Babel 编译为浏览器可执行的 JavaScript。典型配置如下:

    // .babelrc
    {
      "presets": ["@babel/preset-react"]
    }
  2. React 17+ 的优化

    • 无需手动导入 React:编译器自动引入 _jsx 函数。
    • 更简洁的编译输出:减少代码体积,提升可读性。

:::

参考资料

::: details

:::

如何理解 React Fiber 架构?

参考答案

::: details

  1. Fiber 架构的本质与设计目标

Fiber 是 React 16+ 的核心算法重写,本质是基于链表的增量式协调模型。其核心目标并非单纯提升性能,而是重构架构以实现:

  1. Fiber 节点的核心设计

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

// Fiber 节点结构简化示例
const fiberNode = {
  tag: FunctionComponent, // 组件类型
  stateNode: ComponentFunc, // 组件实例或 DOM 节点
  memoizedState: {
    /* Hooks 链表 */
  },
  pendingProps: {
    /* 待处理 props */
  },
  lanes: Lanes.HighPriority, // 任务优先级
  child: nextFiber, // 子节点
  sibling: null, // 兄弟节点
  return: parentFiber, // 父节点
}
  1. Fiber 协调流程(两阶段提交)

阶段 1:Reconciliation(协调/渲染阶段)

阶段 2:Commit(提交阶段)

  1. 优先级调度机制

React 通过 Lane 模型 管理任务优先级(共 31 个优先级车道):

  1. Fiber 架构的优势与局限性

优势

局限性

  1. 与旧架构的关键差异
特性Stack Reconciler(React 15-)Fiber Reconciler(React 16+)
遍历方式递归(不可中断)循环(可中断 + 恢复)
任务调度同步执行,阻塞主线程异步分片,空闲时段执行
优先级控制基于 Lane 模型的优先级抢占
数据结构虚拟 DOM 树Fiber 链表树(含调度信息)

:::

Fiber 结构和普通 VNode 区别

参考答案

::: details

  1. 本质差异
维度普通 VNode(虚拟 DOM)Fiber 结构
设计目标减少真实 DOM 操作,提升渲染性能实现可中断的异步渲染 + 优先级调度
数据结构树形结构(递归遍历)双向链表树(循环遍历)
功能范畴仅描述 UI 结构描述 UI 结构 + 调度任务 + 副作用管理
  1. 数据结构对比

普通 VNode(React 15 及之前)

const vNode = {
  type: 'div', // 节点类型(组件/原生标签)
  props: { className: 'container' }, // 属性
  children: [vNode1, vNode2], // 子节点(树形结构)
  key: 'unique-id', // 优化 Diff 性能
  // 无状态、调度、副作用信息
}

Fiber 节点(React 16+)

const fiberNode = {
  tag: HostComponent, // 节点类型(函数组件/类组件/DOM元素)
  type: 'div', // 原生标签或组件构造函数
  key: 'unique-id', // Diff 优化标识
  stateNode: domNode, // 关联的真实 DOM 节点
  pendingProps: { className: 'container' }, // 待处理的 props
  memoizedProps: {}, // 已生效的 props
  memoizedState: {
    // Hooks 状态(函数组件)
    hooks: [state1, effectHook],
  },
  updateQueue: [], // 状态更新队列(类组件)
  lanes: Lanes.HighPriority, // 调度优先级(Lane 模型)
  child: childFiber, // 第一个子节点
  sibling: siblingFiber, // 下一个兄弟节点
  return: parentFiber, // 父节点(构成双向链表)
  effectTag: Placement, // 副作用标记(插入/更新/删除)
  nextEffect: nextEffectFiber, // 副作用链表指针
}
  1. 协调机制对比
流程VNode(Stack Reconciler)Fiber Reconciler
遍历方式递归遍历(不可中断)循环遍历链表(可中断 + 恢复)
任务调度同步执行,阻塞主线程异步分片,空闲时间执行
优先级控制Lane 模型(31 个优先级车道)
副作用处理统一提交 DOM 更新构建副作用链表,分阶段提交
  1. 能力扩展示例

    a. 支持 Hooks 状态管理

// 函数组件的 Hooks 链表
fiberNode.memoizedState = {
  memoizedState: 'state value', // useState 的状态
  next: {
    // 下一个 Hook(如 useEffect)
    memoizedState: { cleanup: fn },
    next: null,
  },
}

b. 优先级调度实战

c. 副作用批处理

  1. 性能影响对比
场景VNode 架构Fiber 架构
大型组件树渲染主线程阻塞导致掉帧分片渲染,保持 UI 响应
高频更新(如动画)多次渲染合并困难基于优先级合并或跳过中间状态
SSR 水合(Hydration)全量同步处理增量水合,优先交互部分

:::

简述 React diff 算法过程

参考答案

::: details

React Diff 算法通过 分层对比策略启发式规则 减少树对比的时间复杂度(从 O(n³) 优化至 O(n))。其核心流程如下:

1. 分层对比策略

React 仅对 同一层级的兄弟节点 进行对比,若节点跨层级移动(如从父节点 A 移动到父节点 B),则直接 销毁并重建,而非移动。 原因:跨层操作在真实 DOM 中成本极高(需递归遍历子树),而实际开发中跨层移动场景极少,此策略以概率换性能。

2. 节点类型比对规则

a. 元素类型不同

若新旧节点类型不同(如 <div><span>ComponentAComponentB),则:

  1. 销毁旧节点及其子树。
  2. 创建新节点及子树,并插入 DOM。
// 旧树
<div>
  <ComponentA />
</div>

// 新树 → 直接替换
<span>
  <ComponentB />
</span>

b. 元素类型相同

若类型相同,则复用 DOM 节点并更新属性:

// 旧组件(保留实例并更新 props)
<Button className="old" onClick={handleClick} />

// 新组件 → 复用 DOM,更新 className 和 onClick
<Button className="new" onClick={newClick} />

3. 列表节点的 Key 优化

处理子节点列表时,React 依赖 key 进行最小化更新:

a. 无 key 时的默认行为

默认使用 索引匹配(index-based diff),可能导致性能问题:

// 旧列表
;[<div>A</div>, <div>B</div>][
  // 新列表(首部插入)→ 索引对比导致 B 被误判更新
  ((<div>C</div>), (<div>A</div>), (<div>B</div>))
]

此时 React 会认为索引 0 从 A → C(更新),索引 1 从 B → A(更新),并新增索引 2 的 B,实际应仅插入 C。

b. 使用 key 的优化匹配

通过唯一 key 标识节点身份,React 可精准识别移动/新增/删除:

// 正确使用 key(如数据 ID)
<ul>
  {items.map((item) => (
    <li key={item.id}>{item.text}</li>
  ))}
</ul>

匹配规则

  1. 遍历新列表,通过 key 查找旧节点:

    • 找到且类型相同 → 复用节点。
    • 未找到 → 新建节点。
  2. 记录旧节点中未被复用的节点 → 执行删除。

c. 节点移动优化

若新旧列表节点仅顺序变化,React 通过 key 匹配后,仅执行 DOM 移动操作(非重建),例如:

// 旧列表:A (key=1), B (key=2)
// 新列表:B (key=2), A (key=1)
// React 仅交换 DOM 顺序,而非销毁重建

4. 性能边界策略

:::

React 和 Vue diff 算法的区别

参考答案

::: details

React 和 Vue 的 Diff 算法均基于虚拟 DOM,但在实现策略、优化手段和设计哲学上存在显著差异:

1. 核心算法策略对比

维度ReactVue 2/3
遍历方式单向递归(同层顺序对比)双端对比(头尾指针优化)
节点复用类型相同则复用,否则销毁重建类型相同则尝试复用,优先移动而非重建
静态优化需手动优化(如 React.memo编译阶段自动标记静态节点
更新粒度组件级更新(默认)组件级 + 块级(Vue3 Fragments)

2. 列表 Diff 实现细节

a. React 的索引对比策略

b. Vue 的双端对比策略

分四步优化对比效率(Vue2 核心逻辑,Vue3 优化为最长递增子序列):

  1. 头头对比:新旧头指针节点相同则复用,指针后移
  2. 尾尾对比:新旧尾指针节点相同则复用,指针前移
  3. 头尾交叉对比:旧头 vs 新尾,旧尾 vs 新头
  4. 中间乱序对比:建立 key-index 映射表,复用可匹配节点
// 旧列表:[A, B, C, D]
// 新列表:[D, A, B, C]
// Vue 通过步骤3头尾对比,仅移动 D 到头部

3. 静态优化机制

a. Vue 的编译时优化

b. React 的运行时优化

4. 响应式更新触发

框架机制Diff 触发条件
React状态变化触发组件重新渲染父组件渲染 → 子组件默认递归 Diff
Vue响应式数据变更触发组件更新依赖收集 → 仅受影响组件触发 Diff
// Vue:只有 data.value 变化才会触发更新
const vm = new Vue({ data: { value: 1 } })

// React:需显式调用 setState
const [value, setValue] = useState(1)

5. 设计哲学差异

维度ReactVue
控制粒度组件级控制(开发者主导)细粒度依赖追踪(框架主导)
优化方向运行时优化(Fiber 调度)编译时优化(模板静态分析)
适用场景大型动态应用(需精细控制)中小型应用(快速开发)

:::

React JSX 循环为何使用 key

参考答案

::: details

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

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

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

  1. 避免状态混乱

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

// 如果初始列表是 [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 节点。如果使用随机数或索引,每次渲染都会强制重新创建所有元素,导致性能浪费。

:::

React 事件和 DOM 事件区别

参考答案

::: details

  1. 事件绑定方式
  1. 事件对象(Event Object)
  1. 事件传播与默认行为
  1. 性能优化
  1. 跨浏览器兼容性
  1. this 绑定
特性React 事件DOM 事件
命名规则驼峰命名(onClick全小写(onclick
事件对象合成事件(SyntheticEvent原生事件对象
默认行为阻止e.preventDefault()e.preventDefault()return false
事件委托自动委托到根容器需手动实现
跨浏览器兼容内置处理需手动适配
this 指向类组件中需手动绑定默认指向触发元素

React 事件系统通过抽象和优化,提供了更高效、一致的事件处理方式,避免了直接操作 DOM 的繁琐和兼容性问题。

:::

简述 React batchUpdate 机制

参考答案

::: details

React 的 batchUpdate(批处理更新)机制 是一种优化策略,旨在将多个状态更新合并为一次渲染,减少不必要的组件重新渲染次数,从而提高性能。

核心机制

  1. 异步合并更新 当在 同一执行上下文(如同一个事件处理函数、生命周期方法或 React 合成事件)中多次调用状态更新(如 setStateuseStatesetter 函数),React 不会立即触发渲染,而是将多个更新收集到一个队列中,最终合并为一次更新,统一计算新状态并渲染。

  2. 更新队列 React 内部维护一个更新队列。在触发更新的代码块中,所有状态变更会被暂存到队列,直到代码执行完毕,React 才会一次性处理队列中的所有更新,生成新的虚拟 DOM,并通过 Diff 算法高效更新真实 DOM。

触发批处理的场景

  1. React 合成事件onClickonChange 等事件处理函数中的多次状态更新会自动批处理。

    const handleClick = () => {
      setCount(1) // 更新入队
      setName('Alice') // 更新入队
      // 最终合并为一次渲染
    }
  2. React 生命周期函数componentDidMountcomponentDidUpdate 等生命周期方法中的更新会被批处理。

  3. React 18+ 的自动批处理增强 React 18 引入 createRoot 后,即使在异步操作(如 setTimeoutPromise、原生事件回调)中的更新也会自动批处理:

    setTimeout(() => {
      setCount(1) // React 18 中自动批处理
      setName('Alice') // 合并为一次渲染
    }, 1000)

绕过批处理的场景

  1. React 17 及之前的异步代码setTimeoutPromise 或原生事件回调中的更新默认不会批处理,每次 setState 触发一次渲染:

    // React 17 中会触发两次渲染
    setTimeout(() => {
      setCount(1) // 渲染一次
      setName('Alice') // 渲染第二次
    }, 1000)
  2. 手动强制同步更新 使用 flushSync(React 18+)可强制立即更新,绕过批处理:

    import { flushSync } from 'react-dom'
    
    flushSync(() => {
      setCount(1) // 立即渲染
    })
    setName('Alice') // 再次渲染

设计目的

  1. 性能优化 避免频繁的 DOM 操作,减少浏览器重绘和回流,提升应用性能。

  2. 状态一致性 确保在同一个上下文中多次状态变更后,组件最终基于最新的状态值渲染,避免中间状态导致的 UI 不一致。

示例对比

场景React 17 及之前React 18+(使用 createRoot
合成事件/生命周期自动批处理自动批处理
异步操作不批处理自动批处理
原生事件回调不批处理自动批处理

React 的批处理机制通过合并更新减少了渲染次数,但在需要即时反馈的场景(如动画)中,可通过 flushSync 强制同步更新。

:::

简述 React 事务机制

参考答案

::: details

React 的 事务机制(Transaction) 是早期版本(React 16 之前)中用于 批量处理更新管理副作用 的核心设计模式,其核心思想是通过“包装”操作流程,确保在更新过程中执行特定的前置和后置逻辑(如生命周期钩子、事件监听等)。随着 React Fiber 架构的引入,事务机制逐渐被更灵活的调度系统取代。

核心概念

  1. 事务的定义 事务是一个包含 初始化阶段执行阶段收尾阶段 的流程控制单元。每个事务通过 Transaction 类实现,提供 initializeclose 方法,用于在操作前后插入逻辑。例如:

    const MyTransaction = {
      initialize() {
        /* 前置操作(如记录状态) */
      },
      close() {
        /* 后置操作(如触发更新) */
      },
    }
  2. 包装函数 事务通过 perform 方法执行目标函数,将其包裹在事务的生命周期中:

    function myAction() {
      /* 核心逻辑(如调用 setState) */
    }
    MyTransaction.perform(myAction)

在 React 中的应用场景

  1. 批量更新(Batching Updates) 在事件处理或生命周期方法中,多次调用 setState 会被事务合并为一次更新。例如:

    class Component {
      onClick() {
        // 事务包裹下的多次 setState 合并为一次渲染
        this.setState({ a: 1 })
        this.setState({ b: 2 })
      }
    }
  2. 生命周期钩子的触发 在组件挂载或更新时,事务确保 componentWillMountcomponentDidMount 等钩子在正确时机执行。

  3. 事件系统的委托 合成事件(如 onClick)的处理逻辑通过事务绑定和解绑,确保事件监听的一致性和性能优化。

事务的工作流程

  1. 初始化阶段 执行所有事务的 initialize 方法(如记录当前 DOM 状态、锁定事件监听)。
  2. 执行目标函数 运行核心逻辑(如用户定义的 setState 或事件处理函数)。
  3. 收尾阶段 执行所有事务的 close 方法(如对比 DOM 变化、触发更新、解锁事件)。

事务机制的局限性

  1. 同步阻塞 事务的执行是同步且不可中断的,无法支持异步优先级调度(如 Concurrent Mode 的时间切片)。
  2. 复杂性高 事务的嵌套和组合逻辑复杂,难以维护和扩展。

Fiber 架构的演进 React 16 引入的 Fiber 架构 替代了事务机制,核心改进包括:

  1. 异步可中断更新 通过 Fiber 节点的链表结构,支持暂停、恢复和优先级调度。
  2. 更细粒度的控制 将渲染拆分为多个阶段(如 rendercommit),副作用管理更灵活。
  3. 替代批量更新策略 使用调度器(Scheduler)和优先级队列实现更高效的批处理(如 React 18 的自动批处理)。
特性事务机制(React <16)Fiber 架构(React 16+)
更新方式同步批量更新异步可中断、优先级调度
副作用管理通过事务生命周期控制通过 Effect Hook、提交阶段处理
复杂度高(嵌套事务逻辑复杂)高(但更模块化和可扩展)
适用场景简单同步更新复杂异步渲染(如动画、懒加载)

事务机制是 React 早期实现批量更新的基石,但其同步设计无法满足现代前端应用的复杂需求。Fiber 架构通过解耦渲染过程,为 Concurrent Mode 和 Suspense 等特性奠定了基础,成为 React 高效渲染的核心。 :::

理解 React concurrency 并发机制

参考答案

::: details

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

1. 什么是 React 的并发机制?

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

2. 并发机制的工作原理:

3. 并发机制的优势:

4. 如何启用并发模式:

要在 React 应用中启用并发模式,需要使用 createRoot API:

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)

在并发模式下,React 会自动根据任务的优先级进行调度和渲染。

:::

React reconciliation 协调的过程

参考答案

::: details

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

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

规则一:不同类型的元素

规则二:相同类型的元素

规则三:递归处理子节点

  1. 更新真实 DOM
  1. 协调的优化策略

:::

React 组件渲染和更新的全过程

参考答案

::: details

React 组件的渲染和更新过程涉及多个阶段,包括 初始化、渲染、协调、提交、清理 等。以下是 React 组件渲染和更新的全过程,结合源码逻辑和关键步骤进行详细分析。


1. 整体流程概述 React 的渲染和更新过程可以分为以下几个阶段:

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

2. 详细流程分析

(1)初始化阶段

(2)渲染阶段

(3)协调阶段

(4)提交阶段

(5)清理阶段

:::

为何 Hooks 不能放在条件或循环之内?

参考答案

::: details

一个组件中的 hook 会以链表的形式串起来, FiberNode 的 memoizedState 中保存了 Hooks 链表中的第一个 Hook。

在更新时,会复用之前的 Hook,如果通过了条件或循环语句,增加或者删除 hooks,在复用 hooks 过程中,会产生复用 hooks状态和当前 hooks 不一致的问题。

:::

useEffect 的底层是如何实现的(美团)

参考答案

::: details

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

在 React 源码中,useEffect 通过 Fiber 机制 在 commit 阶段 进行处理:

(1) useEffect 存储在 Fiber 节点上

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

(2) useEffect 何时执行

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

React 使用 useEffectEvent() 注册 effect,在 commitLayoutEffect 之后,异步执行 useEffect,避免阻塞 UI 渲染。

(3) useEffect 依赖变化的处理

依赖数组的比较使用 Object.is(),只有依赖变化时才重新执行 useEffect。

在更新阶段,React 遍历旧 effect,并先执行清理函数,然后再执行新的 effect。

简化的 useEffect 实现如下:

function useEffect(callback, dependencies) {
  const currentEffect = getCurrentEffect() // 获取当前 Fiber 节点的 Effect

  if (dependenciesChanged(currentEffect.dependencies, dependencies)) {
    cleanupPreviousEffect(currentEffect) // 先执行上次 effect 的清理函数
    const cleanup = callback() // 执行 useEffect 传入的回调
    currentEffect.dependencies = dependencies
    currentEffect.cleanup = cleanup // 存储清理函数
  }
}

相比 useLayoutEffect,useEffect 是 异步执行,不会阻塞 UI 渲染。

:::


Share this post on:

上一篇文章
小程序
下一篇文章
React 使用