react基础知识总结

136 阅读59分钟

一、基础原理

1. JSX 的本质是什么?与 Vue 模板编译有何区别?

JSX 的本质是 JavaScript 的语法扩展(语法糖) ,它允许在 JavaScript 代码中直接 HTML 风格的标签,最终会被 Babel 等编译器转换为 React.createElement(type, props, children) 函数调用,生成描述 DOM 结构的虚拟 DOM 对象(包含 type、props、key 等属性)。

简单来说,JSX 是虚拟 DOM 的 “语法糖”,它让开发者可以用更直观的方式描述 UI 结构,而无需手动调用 createElement。

与 Vue 模板编译的核心区别

两者的核心差异体现在 编译时机、处理方式和设计理念 上:

    1. 编译时机与优化方式

    • JSX(React):属于 运行时编译。JSX 转换为 createElement 调用后,在浏览器运行时动态生成虚拟 DOM,React 对虚拟 DOM 的优化依赖于运行时的 diff 算法(如 Fiber 架构的任务拆分)。

    • Vue 模板:属于 编译时优化。Vue 模板会在构建阶段被编译为优化后的渲染函数(如静态节点标记、PatchFlag 等),提前确定哪些节点可能变化,减少运行时 diff 的开销。

    1. 逻辑与 UI 的结合方式

    • JSX:通过 JavaScript 逻辑直接控制 UI。JSX 本质是 JavaScript 的一部分,因此循环(map)、条件判断(if/else)、变量引用等逻辑可以直接在 JSX 中通过 JS 语法实现(例如 ****)。

    • Vue 模板:通过 指令抽象逻辑。模板是独立于 JS 的语法,循环(v-for)、条件(v-if)等逻辑需要通过 Vue 提供的指令实现,与 JS 逻辑分离(通过 data、methods 等选项关联)。

    1. 灵活性与约束性

    • JSX:更灵活,允许开发者在 UI 描述中嵌入任意 JS 逻辑,但需要开发者自己控制性能(如手动使用 memo 避免不必要渲染)。

    • Vue 模板:更具约束性,语法规则由 Vue 统一规定,但编译时会自动优化(如静态节点缓存),降低开发者的性能优化成本。

总结:JSX 是 “用 JavaScript 描述 UI” 的语法糖,依赖运行时 diff 优化;Vue 模板是 “独立于 JS 的声明式语法”,依赖编译时优化。两者分别体现了 React “灵活的函数式编程” 和 Vue “渐进式框架” 的设计理念。

2. 虚拟 DOM 为何能提升性能?

其性能提升的本质并非 “虚拟 DOM 本身比真实 DOM 快”,而是通过 减少真实 DOM 操作的代价、优化更新范围与调度机制,从根本上降低浏览器渲染开销,具体可拆解为以下 4 点核心原因:

    1. 核心前提:真实 DOM 操作代价极高,虚拟 DOM 操作轻量

    真实 DOM 不仅是 JavaScript 对象,还与浏览器渲染引擎深度绑定 —— 每一次真实 DOM 操作(如创建、修改、删除节点)都会触发 重排(Reflow,DOM 结构变化导致浏览器重新计算布局)  和 重绘(Repaint,样式变化导致浏览器重新绘制像素) 。这两个过程涉及浏览器底层渲染管线,耗时是纯 JavaScript 操作(如操作虚拟 DOM 对象)的数十倍。

    而虚拟 DOM 是 “纯 JS 对象”,仅包含 DOM 的核心描述(type/props/children),修改虚拟 DOM 本质是修改 JS 变量,无任何浏览器渲染开销,为后续优化奠定基础。

    1. diff 算法找到 “最小更新差异”,避免全量 DOM 替换

    当组件状态变化时,React 不会直接更新真实 DOM,而是先生成新的虚拟 DOM 树,再通过 diff 启发式算法 对比新旧虚拟 DOM 树,精准定位 “必须更新的差异节点”:

  • 按 “同层级对比” 规则,跳过无变化的层级(如父节点不变,直接对比子节点,不递归遍历无关层级);

  • 按 “key 匹配” 规则,列表节点通过唯一 key 找到可复用的节点(如列表重排序时,仅调整节点位置,不重新创建所有节点);

  • 按 “组件类型判断” 规则,不同类型组件直接替换(避免无效的内部 props 对比)。

最终仅将 “差异部分” 转换为真实 DOM 操作,例如:列表中某一项的文本修改,仅更新该文本节点的 textContent,而非全量替换整个列表 DOM,极大减少了真实 DOM 操作的范围。

    1. 批量执行优化:合并多次更新,减少 patch 次数

    React 会自动合并 “短时间内的多次状态更新”(如连续调用 setState、事件回调中的多次更新),避免每次更新都触发独立的虚拟 DOM 生成→diff→patch 流程:

  • 例如:连续执行 setState({ count: 1 }) 和 setState({ count: 2 }),React 会合并为一次更新(直接将 count 设为 2),仅生成一次新虚拟 DOM、执行一次 diff、触发一次真实 DOM patch;

  • 即使在异步场景(如 setTimeoutPromise.then),React 18+ 也通过 “自动批处理” 机制实现合并,进一步减少真实 DOM 操作次数。

    1. Fiber 架构的可中断调度:避免主线程阻塞,提升交互流畅度

旧版 React 的 diff 是 “递归不可中断” 的 —— 若组件树层级深、节点多(如万级列表),diff 会占用主线程数十毫秒,导致用户输入、点击等交互操作卡顿(浏览器主线程同时负责 JS 执行与 UI 渲染,长任务会阻塞渲染)。

Fiber 架构下,React 将 diff 任务拆分为 “小单元”,配合 requestIdleCallback 调度:

  • 优先处理高优先级任务(如用户输入、动画帧),低优先级任务(如列表 diff)在主线程空闲时执行;
  • 若有高优先级任务插入,低优先级 diff 可暂停、保存状态,待主线程空闲后恢复,避免长时间阻塞主线程。

这种调度机制确保 “虚拟 DOM diff 不影响用户交互”,从 “性能体验” 层面提升了应用流畅度。

总结:React 虚拟 DOM 提升性能的核心逻辑是:用 “轻量的 JS 操作(虚拟 DOM)” 替代 “昂贵的真实 DOM 操作”,通过 diff 找最小差异、批量执行减少次数、Fiber 调度避免阻塞,最终降低浏览器渲染开销

需注意:虚拟 DOM 并非 “银弹”—— 对于简单静态页面(如纯展示的官网),直接操作真实 DOM 可能更快;但在中大型项目(如后台系统、复杂交互应用)中,其 “最小更新 + 调度优化” 的优势会显著体现,是性能保障的关键。

3. Fiber 架构解决了什么问题?核心设计是什么?

解决旧版 Stack Reconciler「递归不可中断导致的卡顿」问题。

Fiber 架构是 React 16 对渲染机制的根本性重构,核心目标是解决旧版 Stack Reconciler(栈协调器)的性能瓶颈,同时为 React 并发特性(如优先级调度、可中断更新)提供底层支撑。

一、解决的核心问题

旧版 Stack Reconciler 采用递归同步更新模式,存在两个致命缺陷:

    1. 长任务阻塞主线程,导致页面卡顿

    当组件树层级深(如嵌套数百层)或节点数量庞大(如万级列表)时,递归的“协调(Reconciliation)”过程会持续占用主线程数十甚至数百毫秒。而浏览器主线程同时负责 JS 执行、UI 渲染、用户交互(点击/输入),长任务会直接导致:

    • 页面渲染延迟(卡顿);
    • 用户操作无响应(如输入框打字延迟、按钮点击反馈慢)。
    1. 任务优先级无区分,高优任务无法插队

    所有更新任务(如用户输入、数据请求回调、动画)优先级相同,即使是“用户输入”这种高优任务,也必须等待低优任务(如列表渲染)完成后才能执行,进一步恶化交互体验。

二、核心设计:三大机制针对性突破

    1. 数据结构:链表化 Fiber 节点,支持“中断与恢复”

    将组件树转换为链表结构的 Fiber 节点(替代旧版递归栈),每个节点包含三个关键指针:

    • child:指向子节点(如 <Parent><Child /></Parent> 中,Parent 的 child 是 Child);
    • sibling:指向兄弟节点(如 <div><span></span><p></p></div> 中,span 的 sibling 是 p);
    • return:指向父节点(如 Child 的 return 是 Parent)。

    作用

    • 用“循环遍历 + 指针跳转”替代递归,使任务可在任意节点暂停/恢复(递归一旦开始无法中断);
    • 每个节点作为独立“任务单元”,便于拆分和调度。
    1. 任务调度:时间切片 + 优先级分级,避免主线程阻塞 将更新过程拆分为微小任务单元(每个单元对应一个 Fiber 节点的处理),并通过以下机制实现智能调度:
    • 时间切片:每个任务执行后检查是否超过“剩余时间”(通常 5ms,匹配浏览器 60fps 帧率),超时则暂停,将主线程还给浏览器(处理渲染、用户输入);
    • 优先级分级:通过 scheduler 包定义任务优先级(如 Immediate > UserBlocking > Normal > Low),高优任务(如用户输入)可打断低优任务(如列表渲染);
    • 空闲时间利用:借助 requestIdleCallback 或 React 自研调度器,在浏览器空闲时恢复任务。

    作用:确保 JS 执行不阻塞用户交互和渲染,从根本上解决卡顿问题。

    1. 更新机制:双缓存树(Double Buffering),高效提交更新

    维护两棵 Fiber 树:

    • current 树:对应当前已渲染到 DOM 的状态(“旧树”);
    • workInProgress 树:正在更新的新树(“新树”)。 更新流程
  1. 从 current 树克隆出 workInProgress 树(复用可复用节点);
  2. 在 workInProgress 树上执行更新(处理 props 变化、计算状态等);
  3. 更新完成后,通过 commitRoot 将 workInProgress 树切换为 current 树,一次性批量更新 DOM。

作用

  • 避免更新过程中 DOM 状态不稳定(用户看不到“半成品”UI);
  • 减少 DOM 操作次数(批量提交差异),降低渲染开销。

总结

Fiber 架构的核心价值是让 React 具备“可中断、可恢复、优先级调度”的能力,彻底解决了旧版同步更新的卡顿问题,同时为 React 18+ 的并发模式(如 startTransition、Suspense)提供了底层支撑,最终实现“复杂应用下的流畅交互体验”。

4. React diff 算法的核心规则?与 Vue3 diff 有何差异?

React 的 diff 算法是虚拟 DOM 更新的核心逻辑,其设计遵循 “高效、启发式” 原则,通过减少不必要的对比和 DOM 操作提升性能。而 Vue3 的 diff 算法在继承 Vue2 基础上引入了编译时优化,与 React 形成了显著差异。

一、React diff 算法的核心规则

React 的 diff 基于 “启发式策略”,核心目标是用最少的操作完成虚拟 DOM 到真实 DOM 的更新,主要规则如下:

  1. 层级比较:只对比同层级节点,跨层级直接删除重建

    React 假设 “组件跨层级移动的场景极少”,因此 diff 时只会对同一层级的节点进行对比(如 div 的子节点只和另一个 div 的子节点对比)。若节点跨层级移动(如从父节点的子节点变成祖父节点的子节点),React 会直接删除旧节点,在新位置重建,而非移动节点。

这一规则简化了 diff 复杂度(从 O (n³) 降至 O (n)),但牺牲了跨层级移动的优化(需开发者避免此类场景)。

  1. 类型判断:同类型组件复用,不同类型直接替换

    这一规则进一步减少了不必要的深层对比,尤其适合组件化开发(组件类型通常能反映结构差异)。

  • 若两个节点的类型(如 div、MyComponent)相同,React 会认为它们的结构相似,继续对比其 props 和子节点;

  • 若类型不同,React 会直接删除旧节点,创建新节点(即使结构相似,如 div 和 span 不会复用)。

  1. 列表 diff:依赖 key 实现节点复用,减少错位

    对于列表节点(如 ul 的 li),React 无法通过位置判断节点是否可复用(如列表排序、删除中间项时,位置会变化),因此需要通过 key 作为唯一标识:

注意:禁止用索引作为 key(列表重排序时,索引会变化,导致 key 失效,引发节点错位和状态丢失)。

  • 若新旧列表中存在相同 key 的节点,React 会复用该节点,仅更新差异 props;

  • 若 key 不存在,会创建新节点;若旧节点 key 未在新列表中出现,会删除旧节点。

二、React diff 与 Vue3 diff 的核心差异

Vue3 的 diff 算法在 Vue2 基础上引入了编译时优化,与 React 纯运行时 diff 形成鲜明对比,核心差异体现在以下 4 点:

维度React diffVue3 diff
优化阶段纯运行时优化(diff 过程在浏览器执行)编译时 + 运行时优化(构建阶段提前优化)
静态节点处理无特殊处理,需手动用 memo 缓存编译时标记静态节点(PatchFlag),运行时直接跳过对比
列表 diff 细节基于 key 匹配,通过 “双指针” 查找最长递增子序列优化移动基于 key 匹配,结合编译时生成的 “稳定序列” 信息减少对比
灵活性与自动化更灵活,但需开发者手动优化(如 memo/useMemo)自动化优化(编译时处理),开发者干预少
具体差异解析:
  1. 优化阶段:运行时 vs 编译时
  • React:diff 完全在运行时执行,虚拟 DOM 生成后才开始对比,无法提前预知哪些节点会变化,因此需要遍历所有节点(即使是静态节点)。

  • Vue3:模板编译时会分析节点是否为 “静态”(如纯文本、无动态绑定的节点),并标记动态节点的 “变化维度”(如 PatchFlag: STYLE 表示仅样式变化)。运行时 diff 会直接跳过静态节点,只处理带标记的动态节点,大幅减少对比次数。

  1. 静态节点处理:手动 vs 自动
  • React 中,静态节点(如

    Hello

    )每次渲染都会参与 diff,若要跳过需手动用 React.memo 包裹组件,增加开发者负担。

  • Vue3 中,静态节点在编译时被标记为 “永远不变”,运行时 diff 会直接复用,无需任何手动操作。

  1. 列表 diff 效率:通用策略 vs 编译时增强

两者都依赖 key 复用节点,但 Vue3 额外利用编译时信息:

  • 若列表结构稳定(如无排序 / 删除),Vue3 编译时会生成 “稳定序列” 标记,运行时直接复用所有节点,无需复杂对比;

  • React 无论列表是否稳定,都会执行完整的双指针对比逻辑(查找新增、删除、移动节点)。

  1. 设计理念:灵活优先 vs 优化优先
  • React 优先保证灵活性(JSX 允许嵌入任意逻辑),因此 diff 算法必须兼容所有动态场景,牺牲部分自动化优化;

  • Vue3 优先通过编译时约束(模板语法)实现自动化优化,减少运行时开销,但灵活性略低。

总结

React diff 的核心是 “运行时启发式对比”,通过层级限制、类型判断、key 复用实现高效更新,依赖开发者手动优化静态内容;Vue3 diff 则通过 “编译时标记 + 运行时精准对比” 实现自动化优化,大幅减少不必要的计算。两者分别反映了 “灵活优先” 和 “优化优先” 的框架设计理念。

5. setState 是同步还是异步?批量更新机制如何实现?

要理解 setState 的同步 / 异步特性及批量更新机制,核心需结合 调用场景 和 React 的 更新调度逻辑,以下是详细解析:

一、setState 是同步还是异步?—— 取决于调用场景

setState 并非绝对同步或异步,其执行时机由 React 的 “更新上下文” 决定,主要分为两类场景:

1. 异步批量更新(默认场景)

React 控制的执行上下文 中,setState 是异步的,且会被批量合并,避免频繁渲染。

典型场景

  • React 合成事件(如 onClick、onChange);

  • React 生命周期钩子(如 componentDidMount、componentDidUpdate,注意:componentWillMount 已废弃);

  • React 批量更新 API 内部(如 ReactDOM.unstable_batchedUpdates)。

原因:React 在此类场景下会开启 “批量更新模式”,将多次 setState 的更新请求存入队列,待当前上下文执行完毕后,统一计算最终状态并触发一次渲染,减少 DOM 操作开销。

2. 同步更新(非 React 控制场景)

非 React 控制的执行上下文 中,setState 是同步的,会立即触发更新。

典型场景

  • 原生事件(如 addEventListener 绑定的 click、scroll);

  • 定时器回调(如 setTimeout、setInterval);

  • Promise 回调(如 fetch.then、Promise.resolve().then);

  • 手动同步执行的代码(如直接在控制台调用 setState)。

原因:此类场景脱离了 React 的调度控制,React 无法自动开启批量更新模式,setState 会直接触发同步更新(计算新状态 → 执行 diff → 更新 DOM)。

3. React 18 的关键变化:自动批处理(Automatic Batching)

React 18 之前,仅 “React 控制的同步上下文” 支持批量更新;React 18 引入 自动批处理,将批量更新扩展到 所有场景(包括定时器、Promise 回调等异步场景),统一了 setState 的行为。

示例(React 18)

// React 18 中,setTimeout 内的2次setState会被批量合并
setTimeout(() => {
  this.setState({ count: this.state.count + 1 });
  this.setState({ count: this.state.count + 1 });
  console.log(this.state.count); // 0(异步批量,仍读旧值)
}, 0);

例外:若需强制同步更新,可使用 ReactDOM.flushSync 打破批量:

import { flushSync } from 'react-dom';
flushSync(() => {
  this.setState({ count: this.state.count + 1 });
});
console.log(this.state.count); // 1(强制同步)
二、批量更新机制的实现原理

React 批量更新的核心是 “更新队列 + 批量标记” ,通过控制更新的 “入队” 和 “执行” 时机,实现合并优化,具体步骤如下:

1. 核心标记:isBatchingUpdates

React 内部维护一个全局标记 isBatchingUpdates(布尔值),用于标识 “当前是否处于批量更新模式”:

  • 当进入 React 控制的上下文(如合成事件、生命周期)时,React 会将 isBatchingUpdates 设为 true;

  • 当上下文执行完毕后,将 isBatchingUpdates 设为 false,并触发 “批量执行队列中的更新”。

2. 更新队列:存储待执行的更新请求

每次调用 setState 时,React 会执行以下逻辑:

  1. 创建更新对象:将 setState 的参数(对象或函数)包装为 “更新对象”,包含新状态的计算逻辑;

  2. 判断批量模式

    • 若 isBatchingUpdates 为 true:将更新对象存入当前组件的 “更新队列”,不立即执行;
    • 若 isBatchingUpdates 为 false:直接执行更新队列(计算新状态 → 触发组件重渲染)。
3. 批量执行:合并更新并触发渲染

当 isBatchingUpdates 从 true 变为 false 时(如合成事件执行完毕),React 会:

  1. 合并更新队列:对组件的更新队列进行 “去重合并”,例如多次修改同一状态(如 count),会合并为最终的一次计算(避免中间状态的无效渲染);

  2. 计算新状态:根据合并后的更新队列,计算组件的最终新状态;

  3. 触发重渲染:对组件执行 shouldComponentUpdate → render → diff 虚拟 DOM → 更新真实 DOM,完成一次渲染。

4. React 18 的优化:unstable_batchedUpdates 转正

React 18 之前,开发者若需在非 React 上下文手动开启批量更新,需使用私有 API ReactDOM.unstable_batchedUpdates;React 18 后,该 API 被规范化,且自动批处理默认覆盖所有场景,无需手动调用。

三、总结
  1. setState 的同步 / 异步

    • React 17 及之前:React 控制的上下文(合成事件、生命周期)中异步批量,非 React 上下文(原生事件、定时器)中同步;

    • React 18 及之后:所有场景默认异步批量,仅 flushSync 可强制同步。

  2. 批量更新的核心

通过 isBatchingUpdates 标记控制更新模式,用 “更新队列” 存储待执行请求,最终合并更新并触发一次渲染,本质是 “减少不必要的 DOM 操作,提升性能”。

理解这一机制的关键是:React 优先保证性能,通过批量更新避免频繁渲染;同步 / 异步仅取决于是否在 React 的调度控制范围内

6. state 与 props 的区别?单向数据流的优势?

在 React/Vue 等组件化框架中,state 和 props 是管理组件数据的核心概念,二者职责分明且遵循 “单向数据流” 原则,共同保障组件行为的可预测性。以下从 “核心区别” 和 “单向数据流优势” 两方面详细解析:

一、state 与 props 的核心区别

可从 数据来源、可变性、作用域、更新方式 四个维度清晰区分,结合实例更易理解:

对比维度state(状态)props(属性)
数据来源组件内部主动创建(如 useState 初始化)父组件被动接收(父组件通过属性传递)
可变性可变(组件自身可通过更新函数修改,如 setState)只读(子组件绝对不能直接修改,修改需依赖父组件)
作用域仅在当前组件内部有效(不对外暴露)可在父子组件间传递(支持多层级透传,需配合状态管理工具解决深层透传问题)
更新触发组件自身调用更新函数(如 setCount(c => c+1))父组件更新后重新传递新值,子组件被动接收
  • 父组件的 count 是 state:可通过 setCount 主动修改,是组件的 “内部状态源”;

  • 子组件的 props.count 是只读数据:仅用于渲染,若需修改,必须通过 props.onIncrement 回调让父组件更新,再通过新的 props 传递到子组件。

二、单向数据流的优势

“单向数据流” 指数据只能从 父组件通过 props 流向子组件,子组件无法直接修改父组件的 state(需通过回调间接通知)。这一设计的核心价值是 “让数据变化可追踪、减少不可预期行为” ,具体优势如下:

1. 数据流向清晰,调试效率高

所有数据的 “修改源头” 都集中在父组件(或全局状态管理工具),子组件仅负责 “接收数据→渲染 UI”。若页面出现数据异常,只需沿 “父→子” 的 props 传递链排查,快速定位哪个环节的 state 更新出了问题,避免 “数据在多组件中随意修改” 导致的排查混乱。

例如:若子组件展示的 count 异常,只需检查父组件的 setCount 是否正确调用,无需在子组件中寻找修改逻辑。

2. 避免数据冲突,减少副作用

子组件无法直接修改 props,确保了父组件对数据的 “绝对控制权”。假设允许子组件修改 props,可能出现 “父组件刚更新数据,子组件又偷偷修改” 的冲突,导致父、子组件数据不一致,进而引发渲染异常(如 UI 展示与实际数据不匹配)。

单向数据流通过 “强制子组件依赖父组件更新”,从根源上避免了这种冲突,让组件行为更可预测。

3. 提升组件复用性

子组件仅通过 props 接收 “数据” 和 “行为(回调函数)”,自身不依赖具体的数据源或业务逻辑,因此可在不同场景中复用。

例如:上述 Child 组件可复用在 “商品数量统计”“消息未读计数” 等场景,只需父组件传递不同的 btnText 和 onIncrement 逻辑,组件核心渲染逻辑无需修改。

4. 支撑大型应用的状态管理

在复杂应用(如电商后台、管理系统)中,数据常需在多个组件间共享(如用户信息、全局主题)。单向数据流配合 Redux、Zustand 等状态管理工具,可将全局状态集中管理,所有更新都遵循 “触发动作→修改状态→传递到组件” 的固定流程,支持 “时间旅行调试”(回溯状态变化历史),大幅降低大型应用的维护成本。

总结
  • state 是 “组件内部的可变状态” :由组件自身控制,用于管理组件私有数据;

  • props 是 “父传子的只读数据” :子组件仅使用不修改,是组件间数据传递的桥梁;

  • 单向数据流的核心优势:通过 “父→子” 的严格流向,保证数据变化可追踪、减少冲突、提升复用性,是组件化框架实现 “可维护性” 的关键设计原则。

7. React 事件机制的实现原理?

React 的事件机制是对原生 DOM 事件系统的封装与优化,核心目标是实现跨浏览器一致性、提升性能,并适配组件化开发模式。其实现原理可拆解为三个核心层面:

一、事件委托:全局统一管理事件

React 并未将事件直接绑定到具体的 DOM 元素上,而是采用事件委托机制

  • 所有 React 事件(如 onClick、onChange)最终都会被委托到 React 应用的根容器节点(React 17 前是 document,17 后改为挂载根节点如 #root)。

  • 当用户操作(如点击、输入)触发 DOM 事件时,事件会按照原生 DOM 的 “冒泡机制” 向上传播,最终被根容器的顶层事件监听器捕获。

  • React 再根据事件源(触发事件的 DOM 元素)和组件树结构,定位到对应的组件事件处理函数并执行。

事件委托的优势

  1. 减少内存消耗:无需为每个元素(如列表项、按钮)单独绑定事件监听器,尤其适合动态生成的大量元素(如 map 渲染的列表)。

  2. 动态适配:新增组件(如异步加载的内容)无需重新绑定事件,自动继承委托逻辑。

  3. 集中调度:所有事件通过根容器统一处理,便于后续的跨浏览器兼容和性能优化(如批量处理事件)。

二、合成事件:跨浏览器的统一接口

React 不会直接传递原生 DOM 事件对象,而是传递合成事件(SyntheticEvent) —— 这是对原生事件的标准化封装,具有以下特性:

  1. 跨浏览器一致性

不同浏览器的原生事件接口存在差异(如 IE 用 srcElement,标准浏览器用 target),SyntheticEvent 统一了这些接口,提供一致的属性(target、currentTarget)和方法(preventDefault()、stopPropagation()),开发者无需手动处理浏览器兼容。

  1. 与原生事件的关联

合成事件中可通过 nativeEvent 属性访问原生 DOM 事件(如 e.nativeEvent 即为原生 event 对象),满足特殊场景需求。

  1. 事件池优化(React 17 前)

为减少内存分配开销,React 17 之前会在事件处理完成后回收合成事件对象(清空属性并放入 “事件池”),供后续事件复用。

⚠️ 注意:React 17 后移除了事件池机制,异步代码中可直接访问合成事件属性(无需提前保存)。

三、完整执行流程

当用户触发事件(如点击按钮)时,React 事件的执行分为三个阶段:

1. 原生事件的捕获与冒泡

DOM 元素触发原生事件后,事件会先经过捕获阶段(从 window 向下传播到目标元素),再进入冒泡阶段(从目标元素向上传播到 React 根容器)。

React 事件默认在冒泡阶段处理(若需监听捕获阶段,可使用 onClickCapture 形式)。

2. 根容器捕获事件

事件冒泡到 React 根容器时,被 React 注册的顶层事件监听器(如 dispatchEvent)捕获。

3. 合成事件的分发与执行

React 会执行以下操作:

  • 根据事件类型(如 click)和事件源(触发事件的 DOM 元素),在组件树中查找对应的事件处理函数(如 onClick 回调)。

  • 创建合成事件对象(SyntheticEvent),填充原生事件的关键信息(target、type 等)。

  • 执行找到的事件处理函数,将合成事件对象作为参数传入。

  • 事件处理完成后,若为 React 17 前版本,会回收合成事件对象(放回事件池)。

四、与原生 DOM 事件的核心区别
特性React 合成事件原生 DOM 事件
绑定方式通过 JSX 属性(onClick)声明通过 addEventListener 绑定
委托目标React 根容器(17+)或 document(17 前)具体 DOM 元素
事件对象合成事件(统一接口,跨浏览器兼容)原生事件对象(存在浏览器差异)
阻止冒泡e.stopPropagation()(仅影响 React 事件)e.stopPropagation()(影响原生事件)
总结

React 事件机制的本质是:通过全局事件委托减少监听器数量,通过合成事件统一跨浏览器接口,最终实现高效、一致的事件处理。其设计既优化了性能(减少内存占用、动态适配),又降低了开发复杂度(屏蔽浏览器差异),是 React 组件化模型的重要支撑。

8. 类组件与函数组件的核心差异?2025 年为何优先函数组件?

一、类组件与函数组件的核心差异
对比维度类组件(Class Component)函数组件(Functional Component)
语法本质基于 ES6 Class,需继承React.Component基于纯函数,输入 props 输出 JSX
状态管理依赖this.state与this.setState(),状态合并更新依赖 Hooks(useState/useReducer),独立状态更新
生命周期内置生命周期方法(componentDidMount等)用useEffect模拟,按副作用场景拆分而非固定阶段
this 处理需手动绑定this(箭头函数 / 构造函数绑定)无this,避免上下文指向混乱
代码复用依赖高阶组件(HOC)、render props,嵌套层级深依赖自定义 Hooks,逻辑封装更直观,无嵌套冗余
TypeScript 适配需声明props/state泛型参数,类型推导繁琐支持React.FC自动包含children,类型定义更简洁
性能优化需手动实现shouldComponentUpdate或继承PureComponent用React.memo+useMemo/useCallback精准控制重渲染
二、2025 年优先函数组件的核心原因
1. React 官方生态的明确倾斜
  • 新特性独家支持:React 18 + 的并发渲染(Concurrent Rendering)、自动批处理(Automatic Batching)等核心优化仅对函数组件友好,类组件无法充分利用这些性能提升。

  • 文档与工具链适配:2025 年 React 官方文档已全面以函数组件为核心示例,Create React App、Vite 等构建工具默认生成函数组件模板,类组件仅作为历史兼容内容提及。

2. TypeScript 开发效率碾压
  • 函数组件支持两种简洁的类型定义方式:React.FC自动包含children属性,直接定义 props 类型更灵活可控;而类组件需显式声明props/state双泛型,类型推导容易出错。

  • 2025 年 TypeScript 在 React 项目中渗透率超 90%,函数组件的类型友好性直接降低团队协作成本。

3. Hooks 生态的成熟度与复用性
  • 自定义 Hooks 已成为 React 逻辑复用的事实标准,社区已沉淀海量成熟 Hooks(如useSWR数据请求、useDrag拖拽逻辑),直接适配函数组件。

  • 相比类组件的 HOC 嵌套地狱,Hooks 可将复杂逻辑拆分为独立单元(如分离数据获取与 UI 渲染),代码可维护性提升 40% 以上。

4. 性能与轻量化优势
  • 函数组件无类实例开销,React 18 的并发调度可中断函数组件渲染,而类组件因this状态依赖难以实现中断恢复。

  • 配合React.memo与useCallback,函数组件可实现比PureComponent更精准的重渲染控制,大型应用加载速度平均提升 25%。

5. 未来技术栈兼容性
  • React Server Components(2025 年已大规模应用)仅支持函数组件,可大幅减少客户端 JS 体积,类组件无法适配服务端渲染新范式。

  • 新 Hooks 如useTransition(非阻塞更新)、useDeferredValue(延迟计算)持续扩充函数组件能力,类组件功能停滞不前。

三、类组件的现存价值与适用场景

类组件并未被 React 官方废弃,仍适用于两类场景:

  1. 维护 2019 年前的 legacy 系统,迁移成本过高;

  2. 需直接操作组件实例的特殊场景(如ref获取类实例方法)。

但 2025 年新建项目中,类组件的使用占比已不足 5%。

总结

类组件是 React 的 “传统实现”,依赖面向对象思想管理状态与生命周期;函数组件则是 “现代方案”,通过 Hooks 实现更灵活的逻辑组合与状态管理。2025 年优先选择函数组件,本质是选择更优的开发效率、更好的性能优化、更适配未来的技术生态—— 这既是 React 官方的演进方向,也是行业实践的普遍共识。

9. 受控组件与非受控组件的区别?应用场景?

受控组件:表单值由 state 控制(如value={state}+onChange更新),适合实时校验(如登录表单);

非受控组件:表单值由 DOM 自身管理(用 ref 获取,如defaultValue),适合简单场景(如文件上传)。

10. ref 的作用?useRef 与 createRef 的区别?

一、ref 的核心作用(3 大场景,突破 “数据驱动” 局限)

ref 是 React 中 “直接操作 DOM / 保存持久值” 的补充工具,核心解决数据驱动难以覆盖的问题:

1. 直接获取 DOM 元素 / 组件实例(最常用)
  • 场景:需要操作 DOM 原生能力(如设置焦点、获取输入值、修改样式),跳过状态同步的繁琐流程。

  • 例子:非受控组件中用 ref 获取输入框值,或点击按钮让输入框自动聚焦(文档中 InputWithFocus 示例)。

2. 保存 “跨渲染周期的持久值”
  • 场景:函数组件每次渲染会重置普通变量(如 let timerId = null),而 ref.current 可在组件整个生命周期中保留值,且修改不触发重渲染。

  • 例子:保存定时器 ID(避免重复创建)、记录无需 UI 反馈的点击次数(文档中 Timer 示例)。

3. 避免函数组件的 “闭包陷阱”
  • 场景:useEffect 等 Hooks 形成闭包时,可能访问到旧状态;ref 可实时同步最新值,解决 “状态滞后” 问题。

  • 例子:定时器中需获取最新 count,直接用 count 会因闭包拿到初始值,用 countRef.current 可获取实时值(文档中 Counter 示例)。

二、useRef 与 createRef 的核心区别(4 个关键维度)

两者均用于创建 ref 对象,但因 “是否绑定组件渲染周期”,适用场景完全不同:

对比维度useRef(Hooks 专属)createRef(类组件为主)
使用范围仅函数组件 / 自定义 Hooks 可用类组件(推荐)、函数组件(不推荐,有坑)
渲染稳定性组件生命周期内 仅创建 1 次,ref 对象始终稳定函数组件中 每次渲染都新建(导致 current 丢失);类组件中作为实例属性,仅创建 1 次
功能扩展支持 “保存持久值”+“获取 DOM”仅支持 “获取 DOM”,无持久值能力
核心问题无明显坑点(按规则用即可)函数组件中用会丢失 DOM 关联(致命问题)
关键实例对比(避坑重点)
  • 函数组件用 useRef(正确)

const ref = useRef(null) 仅创建 1 次,组件更新时 ref 对象不变,current 始终指向正确 DOM。

  • 函数组件用 createRef(错误)

const ref = createRef(null) 每次渲染都新建 ref 对象,组件更新后 current 会变为 null(丢失之前关联的 DOM)。

  • 类组件用 createRef(正确)

this.ref = createRef(null) 作为实例属性,仅创建 1 次,current 稳定关联 DOM(文档中 ClassRefDemo 示例)。

三、实战选型总结(一句话判断)
  1. 写函数组件 / 自定义 Hooks → 必用 useRef(兼顾 DOM 操作和持久值,无稳定性问题);

  2. 写类组件 → 用 createRef(通过 this.ref 绑定,避免字符串 ref 的旧写法);

  3. 绝对禁止:在函数组件中用 createRef(会导致 DOM 关联丢失,引发 Bug)。

二、Hooks 进阶

11. Hooks 解决了类组件的哪些问题?

一、解决类组件的 “this 绑定陷阱”—— 彻底告别上下文混乱

类组件的 this 指向依赖组件实例,事件处理、回调函数中常出现 this 丢失问题,需额外处理(如 bind、箭头函数),增加代码冗余且易出错。

Hooks 解决方案:函数组件无 this 概念,所有变量和函数均在函数作用域内,直接引用即可,无需关注上下文指向。

二、解决 “生命周期碎片化”—— 按逻辑拆分而非固定阶段

类组件的生命周期方法(如 componentDidMount/componentDidUpdate/componentWillUnmount)常混杂多段不相关逻辑(如数据请求、事件监听、定时器),导致代码耦合度高、维护困难。

Hooks 解决方案:useEffect 按 “副作用场景” 拆分逻辑(而非固定生命周期阶段),每个 useEffect 处理一段独立逻辑,代码关联性更强。

三、解决 “逻辑复用复杂”—— 告别 HOC 嵌套地狱与 render props 冗余

类组件的逻辑复用依赖高阶组件(HOC)或 render props,易形成 “嵌套地狱”(如 withAuth(withTheme(withData(Component)))),且会增加额外组件层级,调试困难。

Hooks 解决方案:自定义 Hooks 可将复用逻辑抽离为独立函数,组件直接调用即可,无额外组件嵌套,代码更扁平。

四、解决 “状态管理分散”—— 拆分独立状态,简化复杂状态逻辑

类组件的 state 是单一对象,若需管理多组独立状态(如表单中的姓名、年龄、邮箱),需手动合并更新(如 this.setState({ name: newName })),且复杂状态逻辑(如多状态联动)需分散在多个方法中。

Hooks 解决方案

  1. useState 可拆分多组独立状态,互不干扰,更新逻辑更清晰;

  2. useReducer 可将复杂状态逻辑(如多状态联动、条件更新)集中在 reducer 函数中,符合 “单一数据源” 原则。

五、解决 “代码冗余与学习成本”—— 轻量化组件,降低入门门槛

类组件需遵循固定模板(继承 React.Component、render 方法、this 处理),代码冗余;且需理解 “实例生命周期”“this 上下文” 等概念,学习成本高。

Hooks 解决方案

  1. 函数组件结构极简,核心逻辑直接呈现,无模板代码;

  2. 无需理解 “实例”“生命周期阶段”,只需掌握 “状态 Hooks”(useState/useReducer)和 “副作用 Hooks”(useEffect),学习成本大幅降低。

总结:Hooks 对类组件痛点的核心改进
类组件痛点Hooks 解决方案核心价值
this 绑定混乱函数组件无 this,直接引用变量 / 函数减少代码冗余,避免上下文错误
生命周期碎片化useEffect 按逻辑拆分副作用代码关联性更强,易维护、不易遗漏清理
逻辑复用嵌套地狱自定义 Hooks 抽离复用逻辑代码扁平,无额外组件层级
状态管理分散useState 拆分独立状态,useReducer 集中复杂逻辑状态更新更清晰,复杂逻辑可追溯
模板冗余、学习成本高轻量化函数组件,简化概念模型降低入门门槛,提升开发效率

本质上,Hooks 并非 “替代” 类组件,而是通过 “函数式编程思想” 解决类组件在 “逻辑组织、复用、状态管理” 上的固有缺陷,让 React 组件开发更简洁、灵活、可维护。

12. 为何不能在条件语句 / 循环中使用 Hooks?

本质是 React Hooks 依赖 “调用顺序稳定” 实现状态跟踪 的必然约束。破坏这一规则会导致 React 无法匹配 Hooks 与状态的对应关系,引发状态混乱或报错。​

遵循 “顶层调用” 规则,既是保障 Hooks 工作可靠性的前提,也是确保组件行为可预测、易维护的关键 —— 这与 Hooks 设计初衷(简化状态管理、提升代码可维护性)完全一致。

13. useEffect 与 useLayoutEffect 的区别?应用场景?

执行时机不同:

  • useEffect:JS 执行→DOM 渲染→回调执行(异步,不阻塞渲染),适合数据请求、订阅;

  • useLayoutEffect:JS 执行→回调执行(同步,阻塞渲染)→DOM 渲染,适合 DOM 操作(如测量元素尺寸)。

一、核心区别:从浏览器渲染流程看执行时机

要理解两者差异,需先明确浏览器的 “渲染三步曲”:

  1. JS 执行:处理脚本逻辑(如状态更新);

  2. DOM 更新:根据 JS 逻辑修改 DOM 结构(如改变元素位置、样式);

  3. 绘制(Paint) :将更新后的 DOM 渲染到屏幕上,用户可见。

useEffect 与 useLayoutEffect 的核心差异,就体现在它们在 “渲染三步曲” 中的执行位置:

对比维度useEffectuseLayoutEffect
执行时机浏览器完成 “DOM 更新 + 绘制” 后执行(渲染后)浏览器完成 “DOM 更新” 但未 “绘制” 前执行(渲染中)
阻塞性非阻塞:不阻塞浏览器绘制,代码执行时用户已看到新 UI阻塞:会阻塞浏览器绘制,代码执行完才显示新 UI
DOM 操作可见性操作 DOM 会导致 “二次绘制”,可能出现闪烁操作 DOM 后再绘制,用户无感知,避免闪烁
执行环境支持客户端 + 服务器端渲染(SSR)仅支持客户端渲染(SSR 中会警告,因服务器无 DOM)
典型场景数据请求、事件监听、非紧急 DOM 操作紧急 DOM 操作(如计算位置、调整样式)、避免闪烁
二、直观实例:为什么 useLayoutEffect 能避免闪烁?

通过 “动态修改元素位置” 的场景,可清晰看到两者的差异:

1. useEffect 示例(可能出现闪烁)

现象:用户会先看到元素在 left=0 的位置(第一次绘制),随后元素突然跳到 targetLeft 位置(二次绘制),出现 “闪烁”。

原因:useEffect 在第一次绘制后执行,修改 left 会触发第二次 DOM 更新和绘制,两次绘制的差异被用户感知。

2. useLayoutEffect 示例(无闪烁)

现象:用户直接看到元素在 targetLeft 位置,无任何闪烁。

原因:useLayoutEffect 在第一次绘制前执行,修改 left 的状态更新会被 React 合并到本次 DOM 更新流程中,最终只执行一次绘制,用户无感知中间过程。

三、应用场景:如何选择?
1. 优先用 useEffect 的场景(90% 以上的情况)

useEffect 非阻塞的特性不会影响页面渲染性能,适合大多数无需 “紧急 DOM 操作” 的场景:

  • 数据请求:如调用接口获取列表数据(fetch/axios);

  • 事件监听:如监听窗口大小变化(window.resize)、滚动事件(window.scroll);

  • 非紧急 DOM 操作:如修改元素的非视觉关键样式(如 opacity 延迟动画);

  • 清理副作用:如清除定时器、取消事件监听(useEffect 的返回函数)。

核心原则:只要操作不涉及 “用户可见的 DOM 位置 / 样式调整”,或可接受轻微延迟,就用 useEffect。

2. 必须用 useLayoutEffect 的场景(10% 以下的特殊情况)

仅当需要 “在用户看到 UI 前完成 DOM 操作,避免闪烁” 时,才用 useLayoutEffect:

  • 基于 DOM 尺寸 / 位置的动态调整:如弹窗居中(需计算窗口尺寸后设置 top/left)、菜单跟随鼠标位置;

  • 同步修改视觉关键样式:如修改元素的 width/height/position 等直接影响布局的属性;

  • 读取 DOM 计算属性后立即更新状态:如读取 offsetWidth/getBoundingClientRect 后,需同步修改状态以避免二次绘制。

警告:避免在 useLayoutEffect 中执行耗时操作(如复杂循环、大数据处理),否则会阻塞浏览器绘制,导致页面卡顿。

四、额外注意点:清理函数的执行时机

两者的清理函数(返回函数)执行时机也存在差异,需避免踩坑:

  • useEffect 的清理函数:在 “下一次 useEffect 执行前” 或 “组件卸载前” 执行,且不阻塞渲染;

  • useLayoutEffect 的清理函数:在 “下一次 useLayoutEffect 执行前” 或 “组件卸载前” 执行,但会阻塞渲染(因此清理函数也需简洁)。

总结:一句话选型公式
  1. 判断操作是否影响 “用户首次看到的 UI 外观”

    • 不影响 → 用 useEffect(非阻塞,性能更优);

    • 影响(需避免闪烁) → 用 useLayoutEffect(阻塞绘制,保证视觉一致)。

  2. 判断执行环境

    • 服务器端渲染(SSR) → 只能用 useEffect(useLayoutEffect 会报错);

    • 客户端渲染 → 按上述规则选择。

本质上,useLayoutEffect 是 useEffect 的 “紧急版”,专为解决 “渲染闪烁” 而生,但代价是阻塞性能 —— 因此需遵循 “能不用则不用” 的原则,优先选择 useEffect。

14. 如何解决 Hooks 的闭包陷阱(Stale Closure)?

一、先明确:什么是 Hooks 闭包陷阱?

闭包陷阱指 Hooks(如 useEffect、useCallback)中的闭包捕获了组件渲染时的 “变量快照”,而非实时变量值,导致后续执行时访问到的是旧值,与预期不符。

最典型场景:useEffect 内的定时器 / 事件监听,访问到的状态始终是初始值或旧值。

原因:useEffect 仅在组件挂载时执行一次,闭包中的 count 捕获的是挂载时的初始值(0),后续 count 更新不会触发 useEffect 重新执行,闭包无法获取新值。

二、解决方案 1:正确设置 useEffect 依赖项(最基础)

闭包陷阱的常见诱因是 useEffect / useCallback / useMemo 漏写依赖项,导致闭包捕获的变量未随依赖更新而刷新。

解决思路:将闭包中使用的所有变量 / 函数都加入 Hooks 依赖项,确保依赖变化时重新创建闭包,捕获最新值。

关键工具:启用 ESLint 规则 react-hooks/exhaustive-deps(Create React App 默认启用),自动检测漏写的依赖项,避免人为疏忽。

三、解决方案 2:使用 “函数式更新” 获取最新状态(优先推荐)

当状态更新依赖 “最新状态值” 时,避免直接使用闭包中的变量(如 count),改用 函数式更新(setState(prev => prev + 1))。

原理:函数式更新接收的 prev 参数是 React 内部维护的 “最新状态”,不受闭包捕获的旧变量影响,直接基于最新值计算新状态。

实例场景:状态更新依赖自身

适用场景:所有状态更新依赖自身的场景(如计数、累加、切换布尔值),优先用函数式更新,无需依赖闭包中的变量。

四、解决方案 3:用 useRef 保存最新值(特殊场景)

当需要在 不触发 Hooks 重新执行 的前提下获取最新值(如定时器、事件监听中),可通过 useRef 将变量 “持久化”,闭包中直接访问 ref.current(实时最新值)。

原理:useRef 的 current 属性在组件生命周期内保持唯一,且修改不触发重渲染,可实时同步最新变量值,突破闭包的快照限制。

实例修复:定时器中获取最新 count

适用场景

  • 定时器、事件监听(如 window.scroll)中需要最新状态;

  • 闭包所在的 Hooks 无法通过添加依赖项重新执行(如依赖项为空的 useEffect)。

五、解决方案 4:拆分 Hooks,减少闭包复杂度

当一个 useEffect 中包含多个逻辑(如数据请求 + 定时器),易因依赖项冲突导致闭包陷阱。可将逻辑拆分为多个独立 Hooks,使每个 Hooks 的依赖项更清晰,减少闭包捕获旧值的概率。

六、常见误区与避坑建议
  1. 忽略 ESLint 警告:react-hooks/exhaustive-deps 规则会强制检查依赖项,不要随意注释警告(如 // eslint-disable-next-line react-hooks/exhaustive-deps),除非明确理解风险。

  2. 过度依赖 useRef:useRef 是 “兜底方案”,优先用 依赖项优化函数式更新(更简洁、符合 Hooks 设计理念),仅在特殊场景(如不触发 Hooks 重执行)使用 useRef。

  3. 混淆 “状态更新” 与 “闭包刷新” :即使状态更新,若闭包所在的 Hooks(如 useEffect)未重新执行,闭包仍会保持旧值 —— 需通过依赖项变化触发 Hooks 重新执行,进而创建新闭包。

总结:解决方案优先级
  1. 优先:正确设置 Hooks 依赖项(配合 ESLint 规则)+ 使用函数式更新(状态依赖自身时);

  2. 次选:拆分复杂 Hooks,降低依赖项冲突风险;

  3. 兜底:用 useRef 保存最新值(定时器、事件监听等特殊场景)。

本质上,闭包陷阱是 “闭包特性” 与 “Hooks 渲染机制” 共同作用的结果,理解 “每次渲染创建新闭包 + 变量快照” 的原理,再结合上述方案,即可高效规避。

15. useMemo 与 useCallback 的作用?滥用的后果?

一、核心作用:都是 “缓存工具”,但优化对象不同

useMemo 和 useCallback 均用于 减少不必要的计算 / 重渲染,本质是 “空间换时间”—— 通过缓存结果避免重复执行,但优化的对象完全不同:

1. useMemo:缓存 “计算结果”,避免重复计算
  • 作用:缓存函数的返回值(计算结果),仅当依赖项变化时才重新执行函数并更新缓存;若依赖项不变,直接返回缓存的旧结果。

  • 原理:每次渲染时,React 会检查依赖项是否变化:

    • 依赖不变 → 复用上次缓存的结果,不执行函数;

    • 依赖变化 → 执行函数,更新缓存并返回新结果。

  • 适用场景:存在 复杂计算 / 耗时操作(如大数据过滤、排序、循环计算),且计算结果依赖固定变量。

实例:优化大数据排序

未用 useMemo 的问题:即使 data 和 sortKey 未变,组件每次渲染都会重新执行排序函数,浪费性能。

2. useCallback:缓存 “函数引用”,避免子组件无辜重渲染
  • 作用:缓存函数的引用(而非执行结果),仅当依赖项变化时才创建新函数;若依赖项不变,始终返回同一个函数引用。

  • 原理:函数组件每次渲染会重新创建所有内部函数(如 handleClick),导致函数引用变化;useCallback 可固定函数引用,避免因引用变化触发子组件重渲染。

  • 适用场景:函数需 传递给子组件(尤其是用 React.memo 缓存的子组件),且子组件依赖该函数判断是否重渲染。

实例:配合 React.memo 优化子组件重渲染

未用 useCallback 的问题:父组件每次渲染都会创建新的 handleDelete 函数,导致子组件 Item 感知到 onDelete 引用变化,即使逻辑未变也会重渲染。

3. 核心区别:一张表理清
对比维度useMemouseCallback
优化对象函数的返回值(计算结果)函数的引用(函数本身)
返回值缓存的计算结果缓存的函数引用
解决问题避免重复执行复杂计算避免子组件因函数引用变化无辜重渲染
典型搭配复杂数据处理(排序、过滤)React.memo 缓存的子组件
语法格式useMemo(计算函数, 依赖项)useCallback(函数, 依赖项)
二、滥用的后果:性能反降 + 代码冗余

useMemo 和 useCallback 并非 “越多越好”,滥用会带来 额外性能开销代码复杂度提升,甚至抵消优化效果:

1. 增加内存占用:缓存需 “存储成本”
  • 无论是 useMemo 缓存的计算结果,还是 useCallback 缓存的函数引用,都需要 React 在内存中维护缓存对象;

  • 若对 “简单计算”(如 a + b)或 “不传递给子组件的函数” 使用缓存,存储成本会超过优化收益(比如缓存一个简单数字的内存开销,比重新计算一次的耗时更高)。

反例:滥用 useMemo 优化简单计算

2. 增加渲染耗时:依赖项检查有 “计算成本”
  • 每次组件渲染时,React 都需要检查 useMemo/useCallback 的依赖项是否变化(对比依赖数组中每个值的引用);

  • 若依赖项过多(如依赖一个复杂对象 / 数组),或频繁使用缓存 Hooks,依赖检查的总耗时会累积,反而拖慢渲染速度。

反例:依赖项过多的 useCallback

3. 代码冗余:掩盖真正的性能问题
  • 滥用缓存 Hooks 会让代码充斥大量 useMemo/useCallback 模板,降低可读性;

  • 更危险的是:过早使用缓存可能掩盖真正的性能瓶颈(如 “数据获取逻辑不合理”“组件拆分过粗”),导致开发者忽视核心优化方向。

典型误区:新建组件时就给所有函数加 useCallback、所有计算加 useMemo,但实际并无性能问题,属于 “过度优化”。

4. 缓存失效:依赖项处理不当导致 “无效优化”
  • 若依赖项设置错误(如漏写依赖、依赖易变的对象),会导致缓存频繁失效,既没起到优化作用,还多了缓存维护成本;

  • 例如:依赖一个每次渲染都会新建的对象(如 { key: value }),会让 useMemo/useCallback 每次都重新执行,缓存完全失效。

反例:依赖易变对象的 useMemo

三、正确使用原则:“按需优化” 而非 “盲目添加”
  1. 先定位性能问题,再优化

用 React DevTools 的 Profiler 工具 检测是否存在 “不必要的重渲染” 或 “重复执行的复杂计算”,确认有性能瓶颈后再使用缓存 Hooks,避免过早优化。

  1. useMemo:只用于 “复杂计算”

仅当函数执行耗时超过 “缓存维护成本” 时使用(如数据量 > 1000 的排序、多层循环计算);简单计算(如加减乘除、字符串拼接)直接执行,无需缓存。

  1. useCallback:只用于 “传递给子组件的函数”

仅当函数需传递给 React.memo/PureComponent 缓存的子组件时使用;若函数仅在当前组件内部调用(如 onClick 直接写在父组件),无需缓存。

  1. 精简依赖项

依赖项尽量使用 “原始值”(string/number/boolean),避免依赖复杂对象 / 数组;若必须依赖,可通过 useMemo 先缓存依赖项,或提取关键原始值作为依赖。

总结

useMemo 和 useCallback 是 React 性能优化的 “精准工具”,而非 “万能药”:

  • 正确使用可减少重复计算和无效重渲染,提升大型应用性能;

  • 滥用会增加内存开销、渲染耗时和代码复杂度,反而适得其反。

核心原则是: “先测瓶颈,再按需优化” ,让缓存 Hooks 真正解决实际性能问题,而非成为代码负担。

16. 实现一个带批量更新的 useState?

核心用队列存储更新,触发时批量执行:

function useCustomState(initial) {
  const [state, setState] = React.useState(initial);
  const queue = React.useRef([]);
  const isBatching = React.useRef(false);
  const batchSetState = (newState) => {
    queue.current.push(newState);
    if (!isBatching.current) {
      isBatching.current = true;
      // 利用React批量更新机制
      React.startTransition(() => {
        const finalState = queue.current.reduce((acc, updater) => 
          typeof updater === 'function' ? updater(acc) : updater, state
        );
        setState(finalState);
        queue.current = [];
        isBatching.current = false;
      });
    }
  };
  return [state, batchSetState];
}

17. 自定义 Hook 的设计原则?实现 useWindowSize?

原则:①命名前缀use(React 识别 Hook);②抽离复用逻辑(不耦合 UI);③依赖明确。

实现useWindowSize:

function useWindowSize() {
  const [size, setSize] = React.useState({
    width: typeof window !== 'undefined' ? window.innerWidth : 0,
    height: typeof window !== 'undefined' ? window.innerHeight : 0
  });
  React.useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);
  return size;
}

18. useReducer 的适用场景?与 useState 的选择?

一、先明确:useReducer 是什么?—— 状态更新的 “集中管理器”

useReducer 是 React 提供的 “状态管理 Hooks”,基于 “ reducer 模式”(类似 Redux 核心思想),将状态更新逻辑集中到一个函数(reducer) 中,通过 “派发动作(action)” 触发状态更新。核心公式:

(state, action) => newState

  • state:当前状态;

  • action:描述 “要做什么” 的对象(含 type 类型和可选参数);

  • reducer:根据 action.type 计算新状态的纯函数。

二、useReducer 的核心适用场景

当 useState 无法高效管理状态(如逻辑分散、多状态联动)时,useReducer 更具优势,具体场景如下:

1. 多状态联动:多个状态需同步更新

当一个操作需要修改多个关联状态(如表单提交时同时更新 “数据”“加载状态”“错误信息”),useState 需多次调用 setState,逻辑分散;useReducer 可通过一个 action 集中更新所有关联状态,逻辑更连贯。

实例:表单提交状态管理

2. 复杂状态更新逻辑:更新规则多且易变

当状态更新依赖多个条件判断(如购物车商品数量修改:需判断库存、是否限购、是否有优惠券),useState 会导致更新逻辑嵌入组件,代码臃肿;useReducer 可将所有更新规则集中到 reducer 函数,组件仅负责 “派发动作”,逻辑更清晰、易维护。

实例:购物车数量更新

3. 状态操作可追溯:需记录状态变化历史

useReducer 的 “动作(action)” 是可描述、可记录的(如 { type: "UPDATE_QUANTITY", payload: ... }),便于:

  • 调试:通过打印 action 查看状态变化原因;

  • 时间旅行:配合工具(如 Redux DevTools)回溯状态变化历史;

  • 日志记录:记录用户操作轨迹(如 “用户修改商品数量”“用户删除购物车项”)。

useState 无此特性,状态更新是 “黑盒”,无法直接追溯变化原因。

4. 团队协作场景:多人维护同一组件

当多人协作开发一个复杂组件时,useReducer 的 “集中规则 + 动作派发” 模式更易达成共识:

  • 新开发者只需查看 reducer 中的 action.type,即可了解所有支持的状态操作;

  • 新增状态操作时,只需在 reducer 中添加新 case,无需修改组件内多个 setState 调用,降低冲突风险。

三、useState 与 useReducer 的选型对比
对比维度useStateuseReducer
状态复杂度适合简单状态(单一值:数字、字符串、布尔)适合复杂状态(多值联动、嵌套对象)
更新逻辑简单(直接赋值或基于前值简单计算)复杂(多条件判断、多状态同步更新)
可追溯性差(无法记录状态变化原因)好(action 可描述、可记录、可调试)
代码量少(无需定义 reducer,直接调用 setXXX)多(需定义 reducer 和 action)
学习成本低(直观易懂,适合新手)高(需理解 reducer 模式和 action 概念)
适用场景简单表单、计数器、开关组件等复杂表单、购物车、数据列表等
四、选型决策指南:3 步判断用哪个
  1. 第一步:判断状态是否 “简单独立”

    • 若状态是单一值(如 count: 0、isShow: false、name: ""),且更新逻辑简单(如 setCount(c => c+1))→ 用 useState;

    • 若状态是多值联动(如 { data, loading, error })或嵌套对象(如 { user: { name, age }, list: [...] })→ 考虑 useReducer。

  2. 第二步:判断更新逻辑是否 “复杂”

    • 若更新逻辑仅需 1-2 行代码(如 setName(e.target.value))→ 用 useState;

    • 若更新逻辑需多条件判断、循环处理,或需同步更新多个状态 → 用 useReducer。

  3. 第三步:判断是否需要 “可追溯性” 或 “团队协作”

    • 若需调试状态变化、记录操作日志,或多人协作开发 → 用 useReducer;

    • 若个人开发简单组件,无需上述特性 → 用 useState。

五、常见误区:避免过度使用 useReducer
  • 误区 1:所有状态都用 useReducer → 简单状态用 useReducer 会增加不必要的代码量和学习成本(如计数器用 useReducer 纯属冗余);

  • 误区 2:状态多就用 useReducer → 若多个状态是独立的(如 name、age、email 无联动),用多个 useState 比 useReducer 更简洁(如 const [name, setName] = useState(""); const [age, setAge] = useState(0);)。

总结
  • 首选 useState:简单状态、简单更新逻辑、个人开发简单组件;

  • 首选 useReducer:复杂状态联动、复杂更新逻辑、需追溯状态变化、团队协作开发。

核心原则: “够用就好” ,不盲目追求 “高级”,用最简洁的方案解决当前问题 —— 简单场景用 useState 提升开发效率,复杂场景用 useReducer 保证代码可维护性。

19. React 19 的 use 钩子有何作用?如何替代 useEffect 处理异步?

use是新 Hook,支持直接读取 Promise / 上下文,无需嵌套。

替代 useEffect 处理异步:

// 旧:useEffect + 状态
const [data, setData] = useState(null);
useEffect(() => {
  fetchData().then(res => setData(res));
}, []);
// 新:use直接处理
const data = use(fetchData());

注意:use需在组件顶层或自定义 Hook 中使用。

20. useSyncExternalStore 的作用?为何替代 useEffect 订阅外部数据?

用于订阅外部数据源(如 Redux/Zustand),解决 useEffect 订阅的「并发模式下数据不一致」问题。

优势:①自动处理并发更新;②支持服务端渲染;③无需手动清理订阅。

21. 如何用 Hooks 实现类组件的 componentDidCatch?

结合 Error Boundary 组件 +useErrorBoundary库:

import { useErrorBoundary } from 'react-error-boundary';
function MyComponent() {
  const { showBoundary, ErrorBoundary } = useErrorBoundary();
  const fetchData = async () => {
    try {
      await api.getData();
    } catch (err) {
      showBoundary(err); // 触发错误边界
    }
  };
  return (
    <ErrorBoundary fallback={<h1>出错了</h1>}>
      <button onClick={fetchData}>加载</button>
    </ErrorBoundary>
  );
}

22. Hooks 的链表结构是如何实现的?

React 在函数组件渲染时,维护一个currentHook指针,每个 Hook(如 useState)执行时:①创建 Hook 节点(含memoizedState/next指针);②将节点加入链表;③currentHook指向next。下次渲染时按顺序遍历链表获取状态。

三、React 18 + 新特性(8 题)

23. 并发模式(Concurrent Mode)的核心价值?如何启用?

核心价值:「非阻塞渲染」,允许高优先级任务(如输入)打断低优先级任务(如列表渲染),提升交互流畅度。

启用:React 18 默认启用,通过createRoot替代ReactDOM.render。

24. startTransition 的作用?与 setTimeout 的区别?

标记低优先级更新(如搜索结果渲染),不阻塞高优先级任务(如输入)。

区别:①startTransition在 React 调度中执行(可中断);②setTimeout在宏任务队列执行(不可中断,可能延迟)。

示例:搜索框优化:

const [input, setInput] = useState('');
const [results, setResults] = useState([]);
const handleInput = (e) => {
  setInput(e.target.value); // 高优先级
  startTransition(() => {
    setResults(search(input)); // 低优先级
  });
};

25. Suspense 的新用法?如何配合数据请求?

React 18 + 支持「数据请求 Suspense」,无需等待数据加载完成再渲染。

用法:配合react-query等库,数据未加载时显示 fallback:

const DataComponent = React.lazy(() => import('./DataComponent'));
function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <DataComponent /> // 内部用use(fetchData())请求数据
    </Suspense>
  );
}

26. Server Components(RSC)是什么?与客户端组件的区别?

RSC 是在服务端渲染的组件,不发送 JS 到客户端(仅传 HTML),优化首屏加载。

区别:①RSC 无交互能力(不能用 Hooks 如useState);②客户端组件有交互,需 JS 运行;③RSC 可直接访问数据库(服务端环境)。

27. 自动批处理(Automatic Batching)是什么?哪些场景生效?

React 18 默认将多个更新合并为一次渲染(即使在异步回调中)。

生效场景:①setTimeout/Promise.then;②原生事件;③Suspense 回调。

禁用:用flushSync强制同步更新。

28. Transitions 与 DeferredValue 的区别?

同属并发特性,解决不同问题:①startTransition标记「更新操作」为低优先级;②useDeferredValue创建「状态的低优先级副本」(如搜索结果)。

示例:const deferredResults = useDeferredValue(results)。

29. React 19 的 Compiler 有何作用?如何优化代码?

Compiler 是「零配置优化工具」,编译时自动优化代码:①自动 Memoization(无需useMemo/useCallback);②自动批处理;③消除闭包陷阱

注意:需配合 React 19+,无需手动修改代码。

30. 流式渲染(Streaming SSR)的优势?如何实现?

优势:将 SSR 输出拆分为「块」,优先发送 HTML 骨架,再发送动态内容(如数据),减少首屏等待时间。

实现:用ReactDOMServer.renderToPipeableStream替代renderToString,配合 Suspense 使用。

四、性能优化

31. 如何排查组件不必要的重渲染?工具与方案?

工具:React DevTools「Profiler」面板(记录渲染耗时、原因)。

方案:①用React.memo(缓存组件,对比 props);②useMemo/useCallback(缓存值 / 函数);③拆分组件(避免大组件整体重渲染);④用useDeferredValue延迟低优先级更新。

32. React.memo 与 PureComponent 的区别?适用场景?

React.memo是「函数组件的高阶组件」,PureComponent是「类组件的基类」。

核心:均通过「浅比较 props」决定是否重渲染。

适用场景:组件 props 变化少(如列表项、静态组件)。

33. key 的作用?为何不推荐用索引作 key?

key 是 React 识别列表节点的「唯一标识」,用于 diff 算法优化(避免错位复用)。

索引作 key 问题:列表重排序 / 删除时,key 会变化,导致 React 误判节点,引发状态错乱、性能下降。

34. 如何优化千万级数据的表格组件?

虚拟滚动(仅渲染可视区域,如react-window库);②数据分片加载(分页 + 滚动加载);③避免全量重渲染(用memo缓存行组件);④复杂计算迁移至 Web Worker(避免阻塞主线程)。

35. 不可变数据(Immutable)的作用?React 中如何使用?

作用:避免直接修改原数据(导致 diff 无法识别变化),保证数据可追踪。

方案:①用Immer库(produce函数修改数据);②原生方法([...arr]/Object.assign);③ Zustand/Redux Toolkit 内置支持。

36. 商品详情页卡顿,如何排查与解决?

排查:①Profiler 看渲染耗时;②Chrome Performance 看主线程阻塞。

解决:①图片懒加载(react-lazyload);②大型组件拆分;③交互频繁区域用 CSS 硬件加速(transform: translateZ(0));④数据预加载(react-query预请求)。

37. Web Worker 在 React 中的应用场景?如何集成?

应用场景:复杂计算(如数据解析、图表渲染)、大量数据处理(避免阻塞主线程)。

集成:①创建 Worker 文件(处理计算);②组件中用useEffect创建 Worker,监听消息;③卸载时终止 Worker。

五、状态管理与路由

38. Redux、Zustand、Jotai 的区别?2025 年如何选择?

| 方案 | 核心特点 | 适用场景 |

|------------|-------------------------|---------------------------|

| Redux | 单一 store、中间件丰富 | 大型项目(团队协作、复杂流程)|

| Zustand | 轻量、无 Provider、hooks 友好 | 中小型项目(快速开发) |

| Jotai | 原子化状态、按需更新 | 组件间状态共享(避免 props drilling)|

2025 趋势:中小型项目优先 Zustand/Jotai,大型项目用 Redux Toolkit。

39. Redux 中间件的作用?常用中间件有哪些?

作用:扩展 Redux 功能(如异步操作、日志),位于「dispatch 动作」与「reducer 执行」之间。

常用:①redux-thunk(处理异步 action);②redux-saga(复杂异步流,如取消请求);③redux-logger(打印日志)。

40. React Router 6 的核心变化?动态路由如何实现?

核心变化:①Routes替代Switch;②element属性传组件(element={});③useNavigate替代useHistory。

动态路由:①参数路由(/user/:id,用useParams获取);②嵌套路由(Outlet组件渲染子路由)。

41. React Router 的两种模式?实现原理?

Hash 模式:URL 含#,通过hashchange事件监听变化,无服务端配置;②History 模式:用HTML5 History API(pushState/replaceState),需服务端配置(所有路径指向 index.html)避免 404。

42. 如何实现路由权限控制?

方案:①高阶组件封装(withAuth);②路由守卫(React Router 6 用useEffect+navigate);

示例:

function ProtectedRoute({ element }) {
  const isAuth = useAuth();
  const navigate = useNavigate();
  useEffect(() => {
    if (!isAuth) navigate('/login');
  }, [isAuth]);
  return isAuth ? element : null;
}
// 使用:<Route path="/dashboard" element={<ProtectedRoute element={<Dashboard />} />} />

六、工程化与场景题(8 题)

43. React 项目如何捕获错误?Error Boundary 的局限?

组件内错误:用try/catch(同步代码);②渲染错误:用 Error Boundary(类组件,捕获子组件错误);③异步错误:useEffect中try/catch或useErrorBoundary库。

局限:Error Boundary 不能捕获:事件回调错误、异步错误、自身错误。

44. SSR(服务端渲染)的原理?如何避免 hydration 不匹配?

原理:①服务端渲染 HTML(带数据),发送给客户端;②客户端执行 JS,将静态 HTML 激活为交互组件(hydration)。

避免不匹配:①服务端与客户端用同一数据(如将数据注入 window);②禁用客户端独有 API(如 window)在渲染阶段使用;③用 React 18 流式渲染。

45. 如何设计高复用的表单管理方案?

方案:①用react-hook-form(性能优,减少重渲染);②核心功能:表单验证(zod/yup)、动态字段、错误提示、值同步。

示例:useForm Hook + 自定义表单组件(Input/Select)。

46. 万人协同的在线文档编辑器,React 如何设计?

核心:①CRDT 算法(处理并发编辑冲突,如 Yjs 库);②数据分片(避免全文档加载);③Suspense 预加载(优先渲染可视区域);④Web Worker 处理冲突(不阻塞主线程);⑤状态管理用 Zustand(轻量,支持多实例)。

47. React 项目的国际化方案?如何优化性能?

方案:①react-i18next(支持插值、复数、嵌套);②实现:i18n 初始化→useTranslation Hook 获取翻译。

优化:①懒加载语言包(避免全量加载);②缓存翻译结果(useMemo)。

48. Vue 与 React 的设计哲学差异?2025 年如何选择框架?

差异:①Vue「渐进式」(按需使用,模板友好);React「函数式」(JSX 统一逻辑与 UI,生态灵活);②Vue 编译时优化,React 运行时灵活。

选择:①快速开发选 Vue;②复杂交互 / 大型项目选 React(生态丰富,支持并发模式 / RSC)。

49. 如何实现 React 组件的单元测试?常用工具?

工具:Jest(测试框架)+ React Testing Library(组件测试)。

测试点:①渲染正确性(screen.getByText);②交互逻辑(fireEvent.click);③Hook 行为(@testing-library/react-hooks)。

50. React 20 的发展趋势?你如何跟进技术迭代?

趋势:①Compiler 深度优化(零配置性能提升);②RSC 普及(服务端与客户端组件融合);③AI 辅助开发(如 React AI 生成组件)。