渲染与协调
React 将组件渲染为虚拟 DOM 树,再**协调(reconciliation)**到浏览器 DOM。理解该过程有助于解释 key、memo 与性能优化。见 React 基础。
核心概念
React 应用的每次更新都会经历「重新计算应该长什么样」和「把差异应用到真实 DOM」两件事。React 把这两件事拆成明确的两阶段:先在内存里算出新的虚拟 DOM 树并与旧树 diff(协调),再把差异一次性 commit 到真实 DOM。这个设计让 React 可以在 Render 阶段暂停、重新开始、甚至丢弃计算结果而不影响用户看到的界面,是并发特性的基础。
渲染流程(简化)
- 触发更新:
setState、dispatch、父组件重渲染、Context value 变化等。 - Render 阶段:调用组件函数,得到新的 React 元素树(可中断,React 18 并发特性在此阶段调度)。
- Commit 阶段:将变更应用到 DOM,执行
useLayoutEffect、浏览器绘制,再执行useEffect。
setState() → 调度更新 → Render(可中断/可丢弃) → Diff → Commit(不可中断)→ useLayoutEffect → 浏览器绘制 → useEffect
协调与 key
当同一层级的子节点类型或 key 变化时,React 会卸载旧节点、挂载新节点;key 相同则尝试复用实例并更新 props。
- 列表使用稳定唯一
key(如数据库 id)。 - 用下标作 key 在插入/重排时会导致错误的状态保留。
示例:下标 key 导致的状态错位
function TodoList({ todos }: { todos: Todo[] }) {
return (
<ul>
{todos.map((todo, index) => (
// 反例:用 index 作 key,在列表开头插入新项后,
// 原来第 0 项的 <input> 内部状态(如已聚焦、已输入的临时值)会被错误地复用到新的第 0 项上
<li key={index}>
<TodoItem todo={todo} />
</li>
))}
</ul>
);
}
// 正确:用稳定的业务 id
{todos.map((todo) => <li key={todo.id}><TodoItem todo={todo} /></li>)}
React 通过 key 判断「这是不是同一个逻辑实体」,而不是「这是不是同一个位置」——理解这一点就能明白为什么下标 key 在增删排序场景下容易出问题。
Fiber 架构(概念)
React 16+ 使用 Fiber 将渲染工作拆为可调度的小单元,支持:
- 可中断的渲染(为高优先级更新让路)。
Suspense、并发渲染、startTransition等(React 18)。
应用开发通常只需知道「渲染可被打断、分优先级」,无需实现 Fiber 细节。可以把 Fiber 类比成「渲染任务的调度单元」:每个组件对应一个 Fiber 节点,React 可以在处理完一个节点后检查「是否有更紧急的任务插队」,而不必等整棵树处理完。
startTransition
将非紧急更新标记为 transition,保持输入等交互流畅:
import { startTransition, useState } from "react";
function Search() {
const [query, setQuery] = useState("");
const [results, setResults] = useState<Item[]>([]);
function handleChange(value: string) {
setQuery(value); // 紧急更新:输入框要立刻响应
startTransition(() => {
setResults(filterHugeList(value)); // 非紧急:可以稍后计算,不阻塞输入
});
}
// ...
}
配合 useTransition 可以获得 isPending 状态,用于展示加载指示:
function Search() {
const [isPending, startTransition] = useTransition();
const [results, setResults] = useState<Item[]>([]);
function handleChange(value: string) {
startTransition(() => setResults(filterHugeList(value)));
}
return (
<div>
<input onChange={(e) => handleChange(e.target.value)} />
{isPending && <Spinner />}
<ResultList items={results} />
</div>
);
}
Suspense 与数据获取
Suspense 让组件在等待异步资源(懒加载组件、支持 Suspense 的数据获取库)时展示 fallback,而不必手写 if (loading) return <Spinner />:
<Suspense fallback={<Spinner />}>
<UserProfile userId={id} />
</Suspense>
结合支持 Suspense 的数据请求方案(如部分 Relay、use() API 或框架内置能力),可以让加载状态的处理更统一,具体接入方式因所用数据层而异。
StrictMode 与双重渲染
开发模式下 StrictMode 会额外执行一次部分逻辑以帮助发现副作用问题,不代表生产环境会渲染两次。见 React 基础。这个「双重调用」专门用于暴露不纯的组件函数或未正确清理的 Effect——如果一个 Effect 执行两次就出现异常行为(如重复发起请求且没有取消机制),往往说明清理逻辑本身有问题,而不是 StrictMode 的锅。
最佳实践
- 列表
key一律使用稳定的业务标识(id),不要用数组下标,尤其是列表可能重排、插入、删除的场景。 - 筛选、搜索等「输入即触发大量计算」的交互,用
startTransition/useTransition保持输入响应性。 - 理解「Render 阶段可能被多次调用/丢弃」,因此组件函数体必须是纯函数,不要在渲染过程中直接产生副作用(如修改外部变量、发起请求)。
- Suspense 边界的粒度要适当:太粗会导致整块 UI 一起等待,太细会导致 fallback 闪烁过多。
- 排查「明明数据变了但界面没更新」问题时,先检查是否是
key导致组件被错误复用/重建。
常见坑
| 现象 | 常见原因 | 处理 |
|---|---|---|
| 列表重排后输入框内容跑到别的行 | 用了下标作 key | 改用稳定业务 id |
组件函数体内直接调用了 console.log/发请求造成重复执行 | 不了解 Render 阶段可能被多次调用(尤其 StrictMode 开发下) | 副作用移入 useEffect,保持渲染函数纯净 |
startTransition 里的更新看起来「没生效」 | 把同步的紧急更新也包进了 transition,导致响应变慢 | 只把「非紧急、可以稍后计算」的更新放进 transition |
| Suspense fallback 一直不消失 | 内部资源的 Promise 未正确 resolve,或数据层未支持 Suspense 协议 | 确认所用数据获取方案是否真正支持 Suspense |
| 生产环境「感觉」也渲染了两次 | 与 StrictMode 无关,可能是父组件本身多次触发更新 | 用 Profiler 确认渲染次数与触发源 |
延伸阅读
- React 基础:StrictMode 与组件模型
- 运行时优化:减少重渲染
- Effect 与副作用:Commit 阶段与 Effect 执行时机
参考文献
以下链接在编写时均可正常访问:
| 资料 | 说明 |
|---|---|
| React:渲染与提交 | 两阶段 |
| React:preserving state | key 与状态 |
| React:useTransition | 过渡更新 |
| React:Suspense | 异步边界 |
| React:StrictMode | 开发模式检查 |
相关文章
工程化
新建 React 项目时,官方与社区主流工具为 Vite;全栈场景常用 Next.js。涵盖 Vite + React、Create React App 现状、Next.js、目录结构、环境变量、部署、最佳实践与常见坑。
TypeScript
React 官方推荐 TypeScript。类型主要落在 Props、事件、Ref 与 Hooks 返回值上。见 组件。
测试
React 组件测试推荐 React Testing Library(RTL):从用户可见行为断言,而非实现细节。运行器常用 Vitest 或 Jest。涵盖安装配置、查询优先级、异步测试、Mock 网络请求、最佳实践与常见坑。
构建与分包
通过代码分割与构建配置减小首屏 JavaScript 体积。涵盖 React.lazy/Suspense、路由级分割、Tree Shaking、Vite 生产构建、体积分析、最佳实践与常见坑。运行时优化见 runtime。
Effect 与副作用
useEffect 用于在组件渲染后执行与外部系统同步的逻辑(请求、订阅、手动改 DOM 等)。涵盖基本用法、依赖数组、适用/不适用场景、useLayoutEffect、最佳实践与常见坑。
状态管理
React 组件内 state 用 Hooks;跨组件共享状态可选 Context、Redux Toolkit、Zustand 等。见 Context Hook。