# React test

# React 为什么要用 Virtual DOM?它真的一定更快吗?

React 使用 Virtual DOM 的核心目的,不是“绝对更快”,而是:

  1. 提供声明式 UI 编程模型:
    开发者只需要描述“状态对应什么 UI”,不用自己操作真实 DOM。
  2. 让跨平台渲染成为可能:
    React 的核心 reconciler['rekənsaɪlə] 不直接依赖浏览器 DOM,因此可以扩展到 React Native,实现一套代码多端渲染。
  3. 性能优化:
    DOM 操作成本通常高于 JS计算,React 先在内存中Diff,再最小化更新真实 DOM。

但它不一定绝对更快:

  • 在非常简单、变化极少的页面中,直接操作 DOM 可能更直接。
  • Virtual DOM 本身也有计算成本,包含创建 vnode、diff、调度等过程。
  • React 的优势更体现在 复杂交互、可维护性、状态驱动 UI、一致性更新模型 上,而不只是速度。

总结: Virtual DOM 的优势在于:在保证性能的同时,极大地提高了开发效率和代码的可维护性。

# React 中 key 的作用是什么?为什么不能乱用 index?

key 的作用是帮助 React 在同层级节点比较时,识别哪个元素是同一个,从而提升 Diff 的准确性和性能。

React 在列表更新时会根据 key 判断:

  • 节点是复用
  • 节点是新增
  • 节点是删除
  • 节点是否需要移动

如果使用 index 作为 key,在以下场景容易出问题:

  • 列表头部插入元素
  • 列表排序
  • 删除中间某一项

因为这些操作会导致后续项的 index 全部变化,React 会错误地复用节点,可能带来:

  • 输入框内容错位
  • 组件内部 state 错乱
  • 不必要的重新渲染
{list.map((item, index) => (
  <InputItem key={index} value={item.value} />
))}
1
2
3

如果删除第一项,第二项会变成第一项,但 key 也变了,React 可能把旧组件实例错误复用到新数据上。

正确做法

优先使用:

  • 数据库唯一 id
  • 后端返回的稳定唯一标识
  • 本地生成且稳定的 uuid

注意:

key 不是给组件拿 props 用的,它是给 React 内部 Diff 用的,不会通过 props 传递给子组件。

# React Diff 算法的核心策略是什么?

React 没有做传统意义上的完整树编辑距离算法,因为复杂度太高。它采用了基于经验的启发式策略,把 Diff 复杂度从 O(n³) 降到了接近 O(n)。

核心假设有两个:

  1. 不同类型的元素会产生不同的子树 例如 <div> 变成 <span>,React 会直接销毁旧树并重建新树。
  2. 开发者可以通过 key 告诉 React 哪些子元素是稳定的 这样 React 可以高效比较同层级列表。

Diff 的主要规则

1)节点类型不同,直接替换

<div />
<span />
1
2

会直接卸载旧节点、挂载新节点。

2)节点类型相同,比较属性

<div className="a" />
<div className="b" />
1
2

只更新变更的属性。

3)递归比较子节点 对子节点继续执行同样策略。

4)列表比较依赖 key 如果没有 key,只能按顺序比较,遇到头部插入时会产生大量无效更新。

可以补充:

  • React 只做同层比较,不会跨层级移动节点比较。
  • key 的存在,是为了让React 在同级节点比较时,判断是否可以复用以及如何复用。

# React Fiber 是什么?解决了什么问题?

Fiber 是 React 16 引入的新协调架构,本质上是:

把原本不可中断的递归渲染过程,改造成可中断、可恢复、可分优先级调度的任务系统。

它解决的问题

在旧版 React 中,渲染过程一旦开始,就会同步递归执行直到完成。如果组件树很大,会长时间占用主线程,导致:

  • 动画掉帧
  • 输入卡顿
  • 页面失去响应

Fiber 解决方案:

  1. 任务切片 把渲染工作拆成很多小单元,而不是一次性做完。
  2. 可中断 浏览器有更高优先级任务时,可以先暂停 React 工作。
  3. 优先级调度 用户输入、动画这类高优先级任务可以先处理。
  4. 支持并发特性 比如 Concurrent([kənˈkʌrənt]) Rendering、Suspense、Transition 等。

Fiber 节点是什么

Fiber 节点本质是一个 JS 对象,保存:

  • 组件类型
  • props
  • state
  • child(第一个子节点) / sibling(下一个兄弟节点) / return(父节点)
  • effect 信息

它把组件树表示成一个可遍历、可恢复的链表树结构。

总结:

Fiber 不是某个 API,而是 React 底层协调器的重写。好处是让 React 从“同步递归渲染框架”升级成“可调度渲染框架”。

# ReactElement, Fiber, DOM 三者的关系

JSX → ReactElement:
jsx在编译阶段被转换为函数调用(createElement/jsx),在运行时执行函数生成ReactElement对象。

ReactElement → Fiber:
在render阶段,react会根据ReactElement构建Fiber tree

  • 初次渲染:根据ReactElemen创建Fiber节点;
  • 更新阶段:则将新的ReactElement与旧的Fiber进行Diff,生成/更新Fiber节点。
  • 在这个过程中,如果发现变化,会在对应的 Fiber 上做标记(如 Placement、Update、Deletion)
  • 同时将这些带有副作用标记的 Fiber 收集成 effect list。

Fiber → DOM:
在commit阶段,react会遍历 effect list,根据 Fiber 上的标记将变更应用到真实的 DOM 上。

# fiber为什么是链表结构而不是树结构

为了实现可中断、可恢复、可控优先级调度的异步渲染。

  • 1、摆脱递归限制
    • 树结构:通常使用递归遍历,一旦开始,就必须等整个函数调用栈完成才会停止。如果树很深,主线程会被卡死,导致页面卡顿。
    • 链表:将任务拆解为一个个“工作单元”。React 可以在处理完一个 Fiber 节点后,通过 while 循环检查是否有剩余时间。如果没有时间,就先退出循环把控制权还给浏览器,等空闲了再回来。
  • 2、灵活的导航,在链表结构中,每个节点都有三个关键指针:
    • child:深度优先进入子节点。
    • sibling:子节点处理完,横向跳到兄弟节点。
    • return:兄弟节点也处理完了,顺着指针回到父节点继续。
      这种结构让 React 即使在渲染中途停下,也能通过保存的指针精准定位下一步该去哪。
  • 3、支撑并发调度(优先级插队):
    由于是链表结构,react可以给不同的fiber节点分配不同的lanes(优先级)。 当高优先级的任务进来时,react可以丢弃当前链表遍历的进度,优先处理紧急任务,处理完成后再恢复。
  • 4、增量对比的基石:
    通过链表结构将之前的深度递归两棵树变为可调度的增量对比,实现任务的暂停、恢复和优先级调度。

# Fiber 怎么做到让出控制权?

requestIdleCallback:浏览器API,允许开发者在浏览器空闲时间做一些低优先级的任务,而不影响页面渲染。

react使用MessageChannel模拟requestIdleCallback。 为什么不使用原生的requestIdleCallback或者 scheduler.yield呢?

原生的requestIdleCallback不稳定,有兼容性问题。 React Fiber 和 Scheduler 的设计远早于scheduler.yield,而且scheduler.yield兼容性差。而且React 要的是“可控调度”,不只是“让一下”。

# 为什么不用 setTimeout,而用 MessageChannel?

浏览器为了防止定时器滥用,会对setTimeout做最小延迟限制,最少是4ms。 MessageChannel没有最小延迟限制。如果浏览器不支持 MessageChannel,React 才会 fallback 到 setTimeout

# React 的 render 阶段和 commit 阶段有什么区别?

React 更新大致分为两个阶段:

1)Render 阶段

主要做:

  • 根据最新 state/props 计算新的 React 元素树
  • 执行 Diff,找出哪些地方要更新
  • 生成副作用列表

特点:

  • 可以中断
  • 可以重复执行
  • 不应该有副作用

这也是为什么在 render 期间不能做请求、订阅、DOM 操作等副作用:因为render阶段时可中断和可重复执行的,如果执行副作用,可能会导致副作用被执行多次或状态不一致。

2)Commit 阶段

主要做:

  • 把 render 阶段计算出来的变更真正应用到 DOM
  • 执行 ref 更新
  • 执行生命周期 / effect

特点:

  • 不可中断
  • 必须一次性完成
  • 会影响页面可见结果

Hooks 对应关系

  • useMemo / useCallback:render 阶段参与计算
  • useLayoutEffect:DOM 变更后、浏览器绘制前执行
  • useEffect:浏览器绘制后异步执行

总结: 因为render阶段只是计算虚拟DOM diff,不会修改真实DOM,所以可以安全的暂停、恢复和丢弃。而commit阶段会真正修改DOM、更新ref和执行生命周期,这些操作必须保证原子性,如果中断会导致UI状态不一致,因此commit阶段必须同步一次性执行完成。

# React 合成事件

React 合成事件是 React 模拟原生 DOM 事件所有能力的一个事件对象。它的作用是为了解决浏览器兼容性问题。 在 React(17+)中: React 不再把事件统一绑定在 document,而是绑定在 root 容器,在原生事件冒泡到 root 时会触发合成事件,因此合成事件本质上是原生事件冒泡阶段的一个回调。 与原生事件相比有以下好处:

  • 跨浏览器兼容性
  • 批量更新
  • 事件委托(减少内存消耗)
  • 跨平台

阻止原生事件冒泡,会导致合成事件无法触发。
阻止合成事件冒泡:

  • 原生监听器在Root容器内部:不会影响原生事件触发
  • 原生监听器在Root容器外部:会影响原生事件触发

# 为什么React 不再把事件统一绑定在 document,而是绑定在 root 容器

  • 1、解决多 React 版本冲突问题
  • 2、提高应用隔离性
  • 3、保证 stopPropagation 正常

# 为什么说 Hooks 必须按顺序调用?

React 通过 调用顺序 来关联每个 Hook 对应的状态槽位。

例如:

function Demo() {
  const [a, setA] = useState(0);
  const [b, setB] = useState(0);
}
1
2
3
4

React 记住的是:

  • 第 1 个 Hook 是 a
  • 第 2 个 Hook 是 b

如果你把 Hook 写进条件语句:

function Demo({ visible }) {
  const [a, setA] = useState(0);

  if (visible) {
    useEffect(() => {}, []);
  }

  const [b, setB] = useState(0);
}
1
2
3
4
5
6
7
8
9

当 visible 从 true 变成 false 时,Hook 调用顺序发生变化,React 会把原本属于 b 的状态错位绑定,导致状态混乱。

规则

  • 只在函数组件顶层调用 Hook
  • 不要在条件、循环、嵌套函数中调用 Hook
  • 只在 React 函数组件或自定义 Hook 中调用 Hook

总结:
react 内部是通过链表来存储 Hooks 的,通过调用顺序定位状态。如果改变调用规则,会破坏Hooks的调用顺序。

# useEffect 和 useLayoutEffect 的区别是什么?

两者都会在组件渲染后执行副作用,但时机不同。

useEffect

  • 在浏览器完成绘制后执行
  • 不会阻塞页面渲染
  • 适合大多数副作用:
    • 请求
    • 订阅
    • 日志
    • 定时器

useLayoutEffect

  • 在 DOM 更新完成后、浏览器绘制前同步执行
  • 会阻塞浏览器绘制
  • 适合需要读取布局并立即同步修改 DOM 的场景:
    • 获取元素尺寸
    • 滚动位置修正
    • 防止闪烁的布局调整

示例

useLayoutEffect(() => {
  const height = ref.current.offsetHeight;
  ref.current.style.top = `${-height}px`;
}, []);
1
2
3
4

如果用 useEffect,页面可能先闪一下,再调整位置。

总结:

  • 优先用 useEffect
  • 只有在“必须绘制前执行”的场景才用 useLayoutEffect
  • SSR 环境里 useLayoutEffect 还可能有警告问题,需要做兼容处理

# React 18 的并发特性是什么?和“并行”有什么区别?

并发(Concurrent Rendering)本质:任务的可中断性和渲染优先级的调度。
在React18之前,渲染是阻塞式的:一旦开始更新DOM,浏览器无法响应用户输入,直到渲染完成。
并发引入了“可中断的渲染”机制,react可以:

  • 暂停渲染,优先响应用户输入
  • 在内存中准备多个版本的UI,并按优先级调度更新。

它的重点是“可按优先级调度主线程上的渲染工作”。

核心API:

  • useTransition
  • useDeferredValue
  • Suspense 更自然地配合异步 UI

和并行的区别

  • 并发:多个任务在一段时间内交替推进
  • 并行:多个任务在同一时刻同时执行

浏览器 JS 主线程本质上仍是单线程,所以 React 并发更多是调度层面的改进。

总结:
并发并不是让渲染变快了,而是让渲染可中断:知道什么时候该停下来把主线程让给用户,从而解决了大型应用在更新时的卡顿问题。

# React 18 的自动批处理(Automatic Batching)是什么?

批处理指 React 把多个 state 更新合并成一次渲染,以减少不必要的重新渲染(re-render)。

在 React 18 以前,批处理主要发生在 React 事件回调里:

onClick={() => { setA(1); setB(2); }}

这通常只渲染一次。

但在 setTimeout、Promise、原生事件中,旧版本未必会自动批处理。

React 18 开始,更多异步场景也支持自动批处理:

setTimeout(() => { setA(1); setB(2); }, 0);

现在通常也只触发一次渲染。

好处

  • 减少渲染次数
  • 提升性能
  • 行为更一致

注意

如果确实需要立即看到更新结果,可以用 flushSync 强制同步刷新,但要谨慎使用,因为它会破坏调度优化。

# 什么是闭包陷阱(stale closure)?在 React 中怎么解决?

闭包陷阱常见于事件处理函数和Hooks中:函数捕获了旧的 state/props,后续异步执行时拿到的不是最新值。

例子

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1)
    setTimeout(() => {
      console.log(count);
    }, 1000);
  };

  return <button onClick={handleClick}>count: {count}</button>;
}
1
2
3
4
5
6
7
8
9
10
11
12

如果点击后继续更新 count,1 秒后打印的仍是点击之前的旧值。

解决方式

1)函数式状态更新

当你只是基于旧值计算新值时:

setCount(prev => prev + 1);

2)useRef 保存最新值

const latestCount = useRef(count);

useEffect(() => { latestCount.current = count; }, [count]);

异步回调里用 latestCount.current。

3)正确声明依赖

很多 stale closure 问题来自依赖数组不完整,导致 effect/callback 持有旧变量。

注意:
闭包不是 React 的 bug,而是 JS 闭包 + React 渲染模型共同作用的结果。

# useMemo 和 useCallback 的区别与使用场景是什么?

区别

  • useMemo:缓存计算结果
  • useCallback:缓存函数引用

示例

const expensiveValue = useMemo(() => compute(a, b), [a, b]);
const handleClick = useCallback(() => doSomething(a), [a]);
1
2

使用场景

useMemo:

  • 昂贵计算缓存
  • 避免每次 render 重新创建复杂对象
  • 配合 memo 减少子组件重渲染

useCallback:

  • 把函数传给 React.memo 子组件
  • 作为 effect 依赖,避免函数引用频繁变化
  • 避免事件处理器无意义重建导致的联动更新

常见误区

误区 1:到处用就是优化

不是。useMemo/useCallback 本身也有维护依赖和缓存的成本。

误区 2:用了就一定减少渲染

不一定。父组件 render 时子组件默认也可能 render;是否真正减少,要看是否配合 memo、是否命中引用稳定性等。

注意:

useMemo 和 useCallback 是“引用稳定性工具”,不是万金油性能优化按钮。

# React.memo 的原理是什么?什么时候会失效?

React.memo 用于缓存函数组件的渲染结果。当每一项 Props 都没有变化(引用相等)时,React 可以跳过该组件重新渲染。

const Child = React.memo(function Child({ value }) {
  return <div>{value}</div>;
});
1
2
3

默认比较方式

默认是浅比较(shallow compare)

  • 基本类型比较值
  • 引用类型比较地址

所以它容易在以下情况失效:

1)传入了新对象/新数组

<Child style={{ color: 'red' }} />
1

每次 render 都创建新对象,memo 会失效。

2)传入了新函数

<Child onClick={() => {}} />
1

每次 render 函数引用都不同。

3)组件内部状态 (State) 或 Context 更新

即使 props 不变,如果内部有 state 或 context 变了,组件仍会 re-render。

解决办法

  • 对对象用 useMemo
  • 对函数用 useCallback
  • 拆分 context 粒度
  • 必要时传入自定义比较函数
React.memo(Component, (prevProps, nextProps) => {
  return prevProps.id === nextProps.id;
});
1
2
3

注意:
memo 不是阻止组件“渲染”,而是告诉 React 在 props 未变化时可以跳过这次渲染工作。

# 什么情况下会导致组件重新渲染?

常见触发 re-render 的原因:

  1. 组件自身 state 变化
  2. 父组件重新渲染
  3. props 变化
  4. context 值变化
  5. hooks 依赖变化导致内部逻辑更新
  6. 强制刷新

常见误区

误区 1:父组件 render,子组件一定 DOM 更新

不一定。子组件函数可能执行,但最终 DOM 不一定变化。

误区 2:props 没变就不会重新执行

也不一定。如果父组件重新 render,子组件默认也会重新执行,除非用 React.memo 等优化。

注意区分:

  • render phase 执行
  • commit phase DOM 真更新

组件函数执行了,不代表页面真实 DOM 发生变化。

# 受控组件和非受控组件的区别是什么?

受控组件

表单值由 React state 控制:

const [value, setValue] = useState('');

<input value={value} onChange={e => setValue(e.target.value)} />
1
2
3

特点:

  • 数据来源统一
  • 容易做校验、联动、格式化
  • 更符合 React 数据流

非受控组件

表单值保存在 DOM 自身,通过 ref 获取:

const inputRef = useRef();

<input ref={inputRef} />
1
2
3

特点:

  • 更接近原生表单
  • 简单场景写起来更省事
  • 某些场景性能更轻

如何选择

  • 复杂表单、联动校验:优先受控
  • 简单输入、一次性读取:可用非受控
  • 文件上传 input 通常天然偏非受控

注意:
大型业务表单通常会在“受控”和“性能”之间做平衡,比如使用表单库降低重渲染成本。

# 为什么不要直接修改 state?

React 依赖 state 的引用变化来感知更新。如果直接修改原对象,可能导致:

  1. React 无法识别变更,页面不更新
  2. 破坏不可变数据原则,增加调试难度
  3. 导致历史状态不可追踪,不利于优化和回溯

错误示例

const [user, setUser] = useState({ name: 'A' });
user.name = 'B';
setUser(user);
1
2
3

这里对象引用没变,React 可能认为没有变化。

正确写法

setUser(prev => ({ ...prev, name: 'B' }));

不可变更新不仅是 React 的约定,也让:

  • 浅比较优化更容易成立
  • memo / PureComponent 更有效
  • 状态回溯和调试工具更友好

# useRef 有哪些典型用途?

useRef 原理是:初次渲染时在Fiber节点上挂载一个 memoizedState 对象,之后渲染直接返回这个对象,不进行任何比对或更新逻辑。

useRef 返回一个可变对象:

const ref = useRef(initialValue);
1

它的 .current 在整个组件生命周期中保持稳定,修改它不会触发重新渲染。

典型用途

1)获取 DOM 引用

const inputRef = useRef(null);
<input ref={inputRef} />
1
2

2)保存跨渲染周期的可变值

比如:

  • 定时器 id
  • 上一次值
  • 是否首次渲染标记

3)解决 stale closure

保存最新 state/props 给异步回调读取。

4)暴露实例能力给父组件

配合 forwardRef 和 useImperativeHandle。

总结:
useRef 像“组件实例字段”,适合存放不影响视图但需要持久存在的数据。

# forwardRef 和 useImperativeHandle 是做什么的?

函数组件默认不能直接接收 ref 到内部 DOM。forwardRef 可以把父组件传入的 ref 转发下去(可以让父组件拿到子组件的DOM)。

forwardRef

const Input = React.forwardRef((props, ref) => {
  return <input ref={ref} />;
});
1
2
3

父组件就能拿到内部 input。

useImperativeHandle[ɪmˈperətɪv]:

有时不希望父组件直接拿整个 DOM,而是只暴露有限能力:

const Input = React.forwardRef((props, ref) => {
  const innerRef = useRef();

  useImperativeHandle(ref, () => ({
    focus() {
      innerRef.current.focus();
    },
    clear() {
      innerRef.current.value = '';
    }
  }));

  return <input ref={innerRef} />;
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

父组件:

inputRef.current.focus();
1

使用场景

  • 聚焦输入框
  • 打开/关闭弹窗
  • 表单重置
  • 暴露受控的 imperative API

# React Context 的缺点是什么?为什么说它不等于状态管理方案?

使用 Context 的好处是可以更好的管理全局状态,减少 props 过深的问题

但它也有缺点:

  • 1)粒度较粗:当 Provider 的 value 引用变化时,所有消费该 context 的组件都可能重新渲染。
  • 2)不适合高频更新的大型状态:比如表单输入、复杂全局状态,如果全部塞进一个 Context,性能会比较差。
  • 3)可维护性问题:Context 过多或 value 结构过大时,依赖关系不清晰,调试困难。

一个完整的状态管理方案通常还包含:

  • 更细粒度订阅
  • 中间件机制
  • 异步流程管理
  • 调试工具
  • 时间旅行/日志跟踪
  • 模块拆分能力

Context 本质只是状态传递机制,不是完整的状态治理系统。

优化建议

  • 拆分多个 Context
  • 保持 value 引用稳定
  • 高频状态避免直接全局 Context 广播
  • 必要时使用 Zustand、Redux Toolkit、Jotai 等方案

# Redux 为什么要求 reducer 必须是纯函数?

Reducer 的职责是:

根据旧 state 和 action,计算出新 state。

它必须是纯函数,意味着:

  • 相同输入一定得到相同输出
  • 不依赖外部可变状态
  • 不产生副作用
  • 不修改原对象

原因

  • 1)可预测:状态变更逻辑稳定,便于推理和排查问题。
  • 2)方便测试:纯函数天然容易单测。
  • 3)支持时间旅行和日志回放:如果 reducer 有副作用,重放 action 时结果可能不一致。
  • 4)利于性能优化:不可变数据更容易做浅比较。
// 错误示例
function reducer(state, action) {
  state.count++;
  return state;
}

// 正确示例
function reducer(state, action) {
  return { ...state, count: state.count + 1 };
}
1
2
3
4
5
6
7
8
9
10

# 你会怎么做 React 性能优化?

先定位问题 -> 再做优化

1)先定位瓶颈

工具:

  • React DevTools Profiler
  • Performance 面板

先确认到底是:

  • 组件重复渲染
  • 大列表渲染慢
  • 计算开销大
  • 网络慢
  • 资源加载慢

2)常见优化手段

组件层面

  • React.memo
  • useMemo
  • useCallback
  • 合理拆分组件,缩小更新范围
  • 避免状态提升过度

数据层面

  • 保持不可变更新
  • 减少无意义对象/函数新建
  • 精细化状态粒度

列表层面

  • 虚拟列表,如 react-window
  • 正确使用 key
  • 分页 / 懒加载

交互层面

  • 防抖 / 节流
  • useTransition
  • useDeferredValue

资源层面

  • 代码分割 React.lazy
  • 路由懒加载
  • 图片/资源按需加载

3)避免过度优化

  • 不要为了 memo 而 memo
  • 优化前要有数据依据
  • 优化要考虑可维护性

总结:
我一般先用 Profiler 看具体是哪个组件在高频重复渲染,再判断是 props 引用不稳定、或者context 扩散、或者状态设计不合理,还是列表渲染量过大。然后再针对性使用 memo、useMemo、useCallback、虚拟列表、代码分割等手段进行针对性优化。

# 什么是状态提升?它的利弊是什么?

状态提升指把多个组件共享的状态,提升到它们最近的公共父组件管理,再通过 props 传递下去。

优点

  • 单一数据源
  • 便于组件间同步
  • 状态关系更清晰

缺点

  • 容易导致父组件过重
  • props drilling
  • 更新范围扩大,带来性能问题

实践建议

  • 共享范围小:状态提升
  • 层级深:Context
  • 全局复杂:状态管理库
  • 本地 UI 状态尽量本地化,不要一股脑提升

总结:
“不要为了统一管理而过度提升状态”,否则容易造成顶层组件频繁更新。

# React.lazy 和 Suspense 的原理与使用场景是什么?

React.lazy 用于组件懒加载,Suspense 用于在异步组件未加载完成时显示 fallback。

示例

const UserPage = React.lazy(() => import('./UserPage'));

<Suspense fallback={<div>loading...</div>}>
  <UserPage />
</Suspense>
1
2
3
4
5

原理理解

懒加载组件在首次渲染时会触发异步加载,React 暂时无法立即得到组件结果,于是“挂起”当前分支,显示 fallback,等资源准备好后再恢复渲染。

使用场景

  • 路由级代码分割
  • 大模块按需加载
  • 降低首屏 bundle 体积

注意:

  • 不要把 Suspense 边界包得过大,否则一个模块加载会让大片 UI 一起 fallback
  • 要合理设计 loading 边界体验

总结:
Suspense 不只是“loading 组件”,它本质上是一种 React 处理异步渲染依赖的机制。

# useTransition 的作用是什么?

useTransition 用于把某些更新标记为低优先级更新,避免它们阻塞高优先级交互,比如输入。

示例场景

搜索框输入时:

  • 输入框内容更新应立即响应
  • 搜索结果列表渲染可能很重,可以稍后进行
const [isPending, startTransition] = useTransition();

const onChange = e => {
  const value = e.target.value;
  setInput(value);

  startTransition(() => {
    setKeyword(value);
  });
};
1
2
3
4
5
6
7
8
9
10

这里:

  • setInput 是高优先级,保证输入流畅
  • setKeyword 是低优先级,可被打断

适用场景

  • 搜索过滤
  • 大列表切换(>=100 条)
  • 列表排序
  • tab 切换中的重 UI 更新
  • 非紧急内容刷新

它不是“减少计算量”,而是“改善用户感知优先级”。

# useDeferredValue 和 useTransition 有什么区别?

两者都和“延迟非紧急更新”有关,但侧重点不同。

useTransition

用于包裹状态更新动作,告诉 React:这次更新不紧急。

useDeferredValue

用于延迟消费某个值。也就是说,值本身先更新,但某些依赖这个值的重计算内容可以晚一点跟上。

示例

const deferredKeyword = useDeferredValue(keyword);
const filteredList = useMemo(() => {
  return heavyFilter(list, deferredKeyword);
}, [list, deferredKeyword]);
1
2
3
4

输入框可以先响应,复杂过滤稍后完成。

区别总结

  • useTransition:标记某个 state 更新为低优先级,保障高优先级交互。
  • useDeferredValue:延迟状态依赖值的变化,从而避免高频更新导致的性能问题。

总结:
useTransition 适合在触发更新的地方使用,而 useDeferredValue 适合在消费数据的地方使用,当无法控制更新来源时可以使用 useDeferredValue。

为什么要设计两个API?
因为更新发生的位置和计算发生的位置可能不同。

# useDeferredValue 和 debounce的区别

debounce 是基于时间的优化策略,用来减少函数执行次数。通常用来减少请求次数。

useDeferredValue 是 React 的并发调度优化,它会把某个值的更新标记为低优先级,让高优先级交互(比如输入)优先执行,从而避免 UI 卡顿。

debounce 一定会丢弃中间值,而 useDeferredValue 不保证,如果计算很快,不会丢弃,如果计算很慢,中间的 render 被新的render 打断,那么这些未完成的 render 可能会被丢弃,最终只会 commit 最新的状态。

# React 中如何避免 Effect 写成“副作用垃圾场”?

很多项目最大的问题不是不会写 Effect,而是把所有逻辑都塞进 useEffect。

原则

  • 1)先判断是否真的需要 Effect,以下逻辑往往不需要 effect:

    • 从 props/state 派生值
    • 纯计算
    • 事件触发逻辑

    这些通常应放在 render、事件回调、useMemo 中完成。

  • 2)Effect 应聚焦“与外部系统同步”。真正适合 effect 的场景:

    • 网络请求
    • 订阅/取消订阅
    • 定时器
    • DOM API
    • 浏览器事件
    • 第三方库接入
  • 3)一个 Effect 只做一类事,不要在一个 effect 里同时做:

    • 请求
    • 订阅
    • DOM 操作
    • 日志

    应该拆分,便于维护和依赖控制。

  • 4)依赖数组要真实反映使用到的值,不要为了“只执行一次”强行省略依赖,否则极易制造 bug。

总结:
Effect 的本质不是生命周期替代品,而是让 React 状态与外部世界保持同步。

# 自定义 Hook 的价值是什么?

自定义 Hook 的核心价值是:

复用状态逻辑,而不是复用 UI。

例如把“请求 + loading + error + 重试”逻辑封装成:

function useRequest(api) { // ... }

好处

  • 抽离重复逻辑
  • 提高可测试性
  • 提升可读性
  • 保持组件更关注 UI 展示

注意点

  • 自定义 Hook 不是 service 层替代品
  • 不要把它写成“大而全黑盒”
  • 输入输出要清晰,命名语义化

总结:
HOC、render props、hooks 都能做逻辑复用,但 Hooks 在组合性和嵌套使用体验上通常更自然。

# HOC、Render Props、Hooks 三种逻辑复用方案怎么比较?

相同点:都是为了解决代码复用问题

1)HOC(高阶组件):接收一个组件,加上一些逻辑后返回一个新组件,容易产生嵌套地狱

2)Render Props:把逻辑结果通过函数子组件传出去,JSX 嵌套较深。

3)Hooks:可以让你在不编写 Class 的情况下,使用 state,复用状态逻辑

总结:
现代 React 项目里,Hooks 已经是主流方案。HOC 和 Render Props 仍可能在历史项目或特定库中见到。

# React 中怎么设计一个高性能的大列表?

关键思路

  • 1)虚拟化:只渲染可视区域内容,而不是一次渲染几千条。
    常见方案:

    • react-window
    • react-virtualized
  • 2)稳定 key:避免列表更新时错误复用。

  • 3)减少 item 重渲染

    • item 组件 memo
    • props 引用稳定
    • 避免把整个大对象传进去
  • 4)按需加载

    • 无限滚动
    • 分页
    • 懒加载
  • 5)避免复杂计算阻塞

    • 过滤/排序结果缓存
    • 结合 useMemo
    • 必要时配合 useTransition

如果列表项高度动态、包含图片、展开折叠、拖拽排序,需要特别考虑:

  • 动态高度测量
  • 滚动定位修正
  • 缓存策略
  • 滚动性能与交互体验平衡

# 说说 SSR、CSR、SSG 的区别

# CSR(Client Side Rendering)

浏览器先拿到一个基础 HTML,再下载 JS,最后在客户端渲染页面。

优点:

  • 前后端分离体验好
  • 交互强

缺点:

  • 首屏可能慢
  • SEO 较弱

# SSR(Server Side Rendering)

服务器先生成完整 HTML 返回给浏览器,首屏更快、SEO 更好,然后客户端再 hydrate[ˈhaɪˌdreɪt] 接管交互。

优点:

  • 首屏快
  • SEO 友好

缺点:

  • 服务端压力更大
  • 工程复杂度更高

# SSG(Static Site Generation)

构建时预生成 HTML,发布成静态文件。

优点:

  • 极快
  • 成本低
  • SEO 好

缺点:

  • 不适合强实时动态内容
  • 更新内容可能要重新构建

Hydration:
SSR 返回的是可展示 HTML,但要等客户端 JS 执行后,React 才会把静态页面“激活”为可交互应用,这个过程就是 hydration[haɪ'dreɪʃ(ə)n]。

# 什么是 Hydration?Hydration mismatch 为什么会发生?

Hydration 指客户端 React 接管服务端输出的 HTML,并绑定事件、恢复组件树的过程。

mismatch 的原因

当服务端渲染结果和客户端首次渲染结果不一致时,就会出现 hydration mismatch。

常见原因:

  • 服务端用了 window、document
  • 首次渲染依赖随机数、当前时间
  • 服务端和客户端数据不一致
  • 条件渲染分支不同
  • 国际化、时区格式化不一致

错误示例:服务端和客户端时间不同,必然不一致。

<div>{Date.now()}</div>
1

解决思路

  • 保证首屏渲染确定性
  • 浏览器专属逻辑放到 useEffect
  • 时间/随机值做延后处理
  • 保证服务端与客户端拿到相同初始数据

# 什么是错误边界(Error Boundary)?它能捕获哪些错误?

错误边界是 React 中用于捕获子组件树渲染错误的机制。通常通过类组件实现:

  • static getDerivedStateFromError
  • componentDidCatch

能捕获的错误

  • 子组件 render 期间错误
  • 生命周期中的错误
  • 构造函数中的错误

不能捕获的错误

  • 事件处理函数里的错误
  • 异步代码错误(如 setTimeout、Promise)
  • 服务端渲染错误
  • 错误边界自身内部抛出的错误

价值:
避免某个局部组件报错导致整个应用白屏。
现代项目虽然多用函数组件,但错误边界目前依然主要靠类组件或者框架封装实现。

# React 为什么推荐单向数据流?

单向数据流指:

  • 数据从父到子通过 props 传递
  • 子组件不能直接修改父组件状态
  • 状态变更通过事件/回调上报

好处

  • 数据来源清晰
  • 状态流向可预测
  • 更容易调试
  • 降低双向绑定带来的隐式耦合

总结:
React 不是不能双向交互,而是把“视图更新状态”的行为显式化成事件回调,而不是隐式自动同步。

# 如何理解“React 是 UI = f(state)”?

表示:页面是状态的映射结果。在同样的 state 下,组件应该产生同样的 UI 输出。

这体现了 React 的声明式思想:

  • 你不手动操作 DOM
  • 你只描述状态
  • React 负责把状态映射成界面并完成更新

意义

  • 降低命令式 DOM 操作复杂度
  • 提升可预测性
  • 更利于测试和推理

总结:
这也是为什么 render 应该尽量保持纯函数性质。

# 你如何理解 React 中的“可组合性”?

可组合性是 React 很核心的设计理念,指:

通过小而清晰的组件、Hook、上下文、组合模式,把复杂界面逐步搭出来。

体现

  • 组件可以嵌套组合
  • Hook 可以组合逻辑
  • children / slots 模式增强复用
  • 小组件比大组件更容易维护

价值

  • 提升复用
  • 降低复杂度
  • 便于替换和扩展
  • 更适合团队协作

总结:
相比继承,React 更推崇组合,因为组合更灵活、耦合更低。

# React 项目中如何做工程化与架构治理?

  • 1)目录做分层,常见分层:

    • pages
    • components
    • hooks
    • services
    • store
    • utils
    • constants

    避免随意堆文件。

  • 2)状态管理边界 区分:

    • 页面状态
    • 组件局部状态
    • 全局状态
    • 服务端缓存状态

    不要把所有状态都进 Redux/Zustand。

  • 3)请求治理

    • 统一请求层封装
    • 错误处理
    • 重试策略
    • 鉴权与拦截器
    • 请求取消
  • 4)性能治理

    • 路由懒加载
    • 包体积分析
    • 监控渲染性能
    • 长列表优化
  • 5)规范治理

    • ESLint
    • Prettier
    • TypeScript
    • Git hooks
    • 单元测试 / E2E
  • 6)可观测性

    • 错误监控
    • 性能监控
    • 埋点
    • 日志追踪

总结:
按业务边界组织代码,而不是纯技术类型组织

上次更新: 3/22/2026, 5:44:45 PM