React:内核认知
1. React 本质:运行时托管模型
1.1 核心矛盾
JavaScript 函数 = 每次调用,世界重置
React 组件 = 每次调用,状态延续
函数天然无状态。React 解决的是:如何让一个每次都会被重新调用的函数,记住上一次的状态。
答案不在语言层面,而在 React 运行时。
graph TB subgraph "React 运行时" F[Fiber 树] --> H[Hook 链表] F --> R[Reconciler 调度器] R --> D[Commit 阶段] end subgraph "你的代码" C[组件函数] end C -.->|"渲染时调用"| F C -.->|"useState/useRef 等"| H D --> DOM[浏览器 DOM]
1.2 组件 = 函数 + 托管
| 层面 | 组件是什么 |
|---|---|
| JS 语言 | 一个普通函数 |
| React 运行时 | 一个被托管执行的渲染单元,执行前挂 Fiber 上下文,执行后回收结果 |
| 概念上 | 一个接收 props、返回 UI 描述的函数 |
// React 内部简化模型
let currentFiber: Fiber | null = null;
let hookIndex = 0;
function renderComponent(Component: Function, props: any) {
currentFiber = getOrCreateFiberFor(Component); // 挂上下文
hookIndex = 0; // 重置计数器
const jsx = Component(props); // 执行函数
currentFiber = null; // 卸上下文
reconcileAndCommit(jsx); // diff + 提交
}组件不持有状态。状态存在于 Fiber 上。 组件只是”借用”了运行时上下文。
2. Fiber:React 架构基石
2.1 为什么需要 Fiber
React 16 之前,渲染是递归且不可中断的。一旦开始渲染整棵组件树,必须一口气完成,JS 线程被占用,无法响应用户输入。
Fiber 将渲染分解为可中断的工作单元:
| 概念 | 说明 |
|---|---|
| Fiber 节点 | 对应一个组件实例,是一个 JS 对象 |
| Fiber 树 | 通过 child(第一个子节点)、sibling(下一个兄弟)、return(父节点)组成的链表树 |
| workLoop | 循环遍历 Fiber 树,每次处理一个节点,处理完检查是否需要让出线程 |
| 优先级 | 用户输入 > 动画 > 数据更新,高优先级可打断低优先级 |
graph TB subgraph "Fiber 节点结构" F[Fiber Node] F --> tag["tag: 类型标记<br/>(FunctionComponent/ClassComponent/HostComponent...)"] F --> stateNode["stateNode: 对应的 DOM 或组件实例"] F --> child["child: 第一个子 Fiber"] F --> sibling["sibling: 下一个兄弟 Fiber"] F --> return["return: 父 Fiber"] F --> memoizedState["memoizedState: Hook 链表头"] F --> pendingProps["pendingProps / memoizedProps"] F --> flags["flags: 副作用标记<br/>(Placement/Update/Deletion...)"] end
2.2 双缓冲(Double Buffering)
React 同时维护两棵 Fiber 树:
| 树 | 说明 |
|---|---|
| current | 当前显示在屏幕上的 Fiber 树 |
| workInProgress | 正在构建的新 Fiber 树 |
sequenceDiagram participant C as current 树 participant W as workInProgress 树 participant D as DOM Note over C: 当前屏幕对应这棵树 Note over W: 开始新一轮渲染 W->>W: 创建/复用 Fiber 节点 W->>W: 执行组件函数 W->>W: diff 对比 Note over W: commit 阶段 W->>D: 更新 DOM Note over C,W: 指针交换:W 变成新的 current
每次状态更新触发 workInProgress 树的构建,构建完成后在 commit 阶段一次性更新 DOM,然后指针交换——workInProgress 变成新的 current 树。
3. Hook:将状态从函数”抽出”
3.1 核心命题
每个 Hook 调用都是一个”数据插座”——函数执行到这一行时,从外部(Fiber 链表)插入持久化数据,执行完再存回去。
3.2 数据结构
Fiber 节点
│
└─ memoizedState ─→ Hook[0] Hook[1] Hook[2]
(链表头) │ │ │
next ───────→ next ────────→ next → null
每个 Hook 节点内部结构:
interface Hook {
memoizedState: any; // 存储的值(useState 存值,useRef 存 {current} 对象,useEffect 存 effect 对象)
baseState: any; // 基础状态
baseQueue: Update<any> | null; // 更新队列
queue: UpdateQueue<any> | null; // 当前更新队列
next: Hook | null; // 下一个 Hook 节点
}3.3 映射机制:靠调用顺序
Hook 与链表节点的对应关系完全靠调用顺序,不靠名称:
// 第 1 次渲染
useState(0); // → Hook[0]
useState('a'); // → Hook[1]
useEffect(...); // → Hook[2]
// 第 2 次渲染(顺序必须一致)
useState(0); // → 匹配 Hook[0](忽略初始值 0)
useState('a'); // → 匹配 Hook[1](忽略初始值 'a')
useEffect(...); // → 匹配 Hook[2]这就是不能在条件/循环中使用 Hook 的根本原因。 顺序乱了,后面的 Hook 全都会匹配到错误的状态。
3.4 自定义 Hook 是”宏展开”
自定义 Hook 的边界只存在于你的代码中。React 运行时完全不知道”这是个自定义 Hook”:
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 }); // → Hook[a]
useEffect(() => {
/* ... */
}, []); // → Hook[a+1]
return size;
}
function MyComponent() {
const size = useWindowSize(); // 展开为 Hook[0], Hook[1]
const [name, setName] = useState(""); // Hook[2]
}Fiber 链表上总是扁平结构,没有层级。 自定义 Hook 只是函数抽取,零运行时开销。
3.5 各类 Hook 在链表上的存储
Hook 都利用同一个机制——链表持久化——区别在于存什么和目的:
| Hook | 存了什么 | 目的 |
|---|---|---|
useState | 状态值 + 更新队列 | 跨渲染保持值 |
useReducer | 状态值 + 待处理 action 队列 | 跨渲染保持值(reducer 模式) |
useRef | { current: value } 对象引用 | 跨渲染保持可变引用(不改 ref 不触发渲染) |
useEffect | 上次 deps + cleanup 函数 | 对比依赖,决定是否重新执行 |
useLayoutEffect | 同上 | 同上,但同步执行(paint 前) |
useInsertionEffect | 同上 | CSS-in-JS 库在 DOM 修改前插入样式 |
useMemo | 缓存值 + 上次 deps | 跳过重复计算 |
useCallback | 缓存函数 + 上次 deps | 跳过函数重建 |
useContext | Context 引用 | 订阅上下文变化 |
三类目的:
1. 持久化值(useState / useReducer / useRef)→ "记住上次是啥"
2. 依赖对比(useEffect / useLayoutEffect)→ "和上次比变了没有"
3. 混合型(useMemo / useCallback)→ 先对比,没变就返回缓存值
3.6 简易实现
// React 内部的极度简化模型
let currentFiber: any = null;
let hookIndex = 0;
function useState<T>(initial: T): [T, (val: T) => void] {
const fiber = currentFiber;
const hooks: any[] = fiber._hooks;
if (hookIndex >= hooks.length) {
hooks.push({ state: initial });
}
const hook = hooks[hookIndex];
const setState = (newVal: T) => {
hook.state = newVal;
scheduleRerender(fiber);
};
hookIndex++;
return [hook.state, setState];
}4. 数据流:Props、Context 与状态管理
4.1 共性:都为解决”函数每次全新调用,数据从哪来”
组件是函数,每次渲染都是全新调用。数据需要从某个地方注入到函数中。三种方案,本质是三种寻址方式:
graph TD subgraph "① Props:显式传参" P_P[父组件] -->|"调用时传参"| P_C[子组件] end subgraph "② Context:Fiber 树向上搜索" C_P[Provider] -.->|"后代组件通过 Fiber.return 向上查找"| C_C[消费者] end subgraph "③ 状态管理库:外部 Store" S[Store] -->|"订阅"| A[组件A] S -->|"订阅"| B[组件B] S -->|"订阅"| C[组件C] end
| 方式 | 寻址机制 | 方向 | 决定时机 |
|---|---|---|---|
| props | 函数参数传递 | 父 → 子(显式) | 运行时调用 |
| Context | Fiber.return 向上遍历 | 子 → 祖(隐式) | 运行时组件树位置 |
| 状态管理库 | 外部 Store 订阅 | 任意方向(发布-订阅) | 运行时订阅关系 |
4.2 Props:单向显式传递
最简单、最直接的数据流。父组件渲染时把数据作为参数传给子组件函数。
// 数据从 A → B → C → D,每个中间层都必须参与
function A() { return <B count={count} />; }
function B({ count }) { return <C count={count} />; } // 仅转发
function C({ count }) { return <D count={count} />; } // 仅转发
function D({ count }) { return <span>{count}</span>; }优点:数据流显式可追踪,类型安全,无隐式依赖。 缺点:prop drilling——中间组件被迫转发自己不需要的数据。
4.3 Context:短路传递
跳过中间节点,让后代组件直接在 Fiber 树上”搜索”祖先提供的数据。
const CountContext = createContext(0);
function A() {
return <CountContext.Provider value={count}>
<B /> {/* B、C 不需要知道 count */}
</CountContext.Provider>;
}
function D() {
const count = useContext(CountContext); // 直接在 A 提供的数据上"寻址"
}优点:消除 prop drilling。 代价:
- Provider value 变化 → 所有后代消费者全部重渲染,memo 挡不住
- 数据来源隐式,调试困难
- 不适合高频更新的数据(如每秒变化的计数器)
graph TD subgraph "Context 的渲染代价" P["Provider 新 value"] -->|"标记全部"| C1["消费者 ✓ 重渲染"] P -->|"标记全部"| C2["消费者 ✓ 重渲染"] P -->|"标记全部"| C3["消费者 ✓ 重渲染"] P -->|"中间层"| N1["非消费者<br/>可被 memo 跳过"] end
4.4 状态管理库:外部 Store + 发布-订阅
Context 管的是”祖先给后代”,状态管理库管的是”任意组件之间共享状态”。
graph TD subgraph "发布-订阅模型" Store["外部 Store<br/>{ count: 0 }"] X["组件 X<br/>更新 count = 5"] --> Store Store -->|"通知"| Y["组件 Y<br/>重渲染"] Store -->|"通知"| Z["组件 Z<br/>重渲染"] Store -->|"不通知"| W["组件 W<br/>没有订阅 count"] end
内核——为什么需要它:
Context 的渲染模型是”全量通知”——Provider value 一变,所有消费者全部重渲染。对于高频更新或大量消费者的场景,这是性能灾难。
状态管理库通过精确订阅解决这个问题:
Context 模型: Provider 变了 → 所有消费者全量渲染 → 靠 memo 过滤(不一定有效)
状态管理库模型: Store 中某个字段变了 → 只通知订阅了这个字段的组件
以 Zustand 为例:
// 只有使用到 count 的组件会重渲染
const useStore = create((set) => ({
count: 0,
name: "foo",
setCount: (n) => set({ count: n }),
}));
// 组件 A:订阅 count → count 变 → 重渲染
function A() {
const count = useStore((s) => s.count); // 选择器,精确订阅
}
// 组件 B:订阅 name → count 变 → 不重渲染
function B() {
const name = useStore((s) => s.name);
}4.5 三者选型策略
graph TD Q["需要跨组件共享数据?"] -->|"否"| P["props,直接传"] Q -->|"是"| Q2["更新频率?"] Q2 -->|"低频<br/>(主题、语言、用户信息)"| C["Context"] Q2 -->|"高频<br/>(表单状态、实时数据)"| Q3["数据规模?"] Q3 -->|"简单"| S["拆成多个 Context<br/>每个管一个值"] Q3 -->|"复杂"| L["状态管理库<br/>(Zustand / Jotai 等)"]
| 场景 | 用什么 | 原因 |
|---|---|---|
| 父 → 子 直传 | props | 最简单,零额外机制 |
| 全局配置(主题、语言、当前用户) | Context | 低频变化,全量重渲染可接受 |
| 表单多字段共享 | Context + useReducer | 中等复杂度,Context 足够 |
| 高频更新的列表/表格状态 | 状态管理库 | 精确订阅,避免全量渲染 |
| 跨页面、跨路由的状态 | 状态管理库 | Context 跨不同 Provider 子树困难 |
| 服务端缓存(API 数据) | React Query/SWR | 这不是状态管理,是缓存 |
4.6 本质统一
// props:调用的那一刻,父组件传进来
const value = props.data;
// Context:沿 Fiber 树向上寻址,在最近的 Provider 上取值
const value = useContext(SomeContext);
// 状态管理库:从外部 Store 中按选择器取值,并订阅变化
const value = useStore(selector);三者都是”函数组件不持有状态”这一前提下的补偿机制。 区别只是:数据从哪个外部位置注入到函数中、以及变化时通知谁。props 靠调用者传,Context 靠 Fiber 树向上找,状态管理库靠发布-订阅。三种寻址方式对应三种数据流动场景。
5. 渲染管线(Render Pipeline)
5.1 三个阶段
graph LR subgraph "Trigger" T1["setState"] --> R T2["props 变化"] --> R T3["父组件渲染"] --> R T4["Context 变化"] --> R end subgraph "Render 阶段(可中断)" R["执行组件函数"] --> D["生成/更新 Virtual DOM"] D --> DF["diff 对比(Reconciliation)"] end subgraph "Commit 阶段(不可中断)" DF --> M["Mutation: 更新 DOM"] M --> L["Layout: 执行 useLayoutEffect"] end subgraph "Post-Commit" L --> P["浏览器绘制(Paint)"] P --> E["异步执行 useEffect"] end
5.2 渲染触发条件
graph TD S[setState 调用] -->|唯一真正的触发器| R[调度重渲染] R --> F[标记 Fiber 节点] F --> B["从标记节点向上追溯到根节点<br/>(因为 props 或 context 可能沿途变化)"] B --> W[workLoop: 从根开始遍历]
父组件渲染必然导致子组件渲染(默认行为,除非用 memo 显式阻止)。但渲染 ≠ DOM 更新——渲染是执行函数,DOM 更新是 diff 后的结果。
5.3 批处理(Batching)
同一事件处理函数中的多次 setState 自动合并为一次渲染:
// 一次点击,一次渲染(React 18+ 自动批处理,包括 setTimeout/Promise 等异步回调)
const handleClick = () => {
setName("A"); // ─┐
setAge(25); // ─┤ 合并为一次渲染
setCount((c) => c + 1); // ─┘
};5.4 useEffect vs useLayoutEffect 时序
graph LR subgraph "useEffect" E_R["Render"] --> E_D["DOM Update"] --> E_P["Browser Paint"] --> E_E["useEffect"] E_E --> E_R2["Re-render (若有 setState)"] --> E_D2["DOM Update"] --> E_P2["Paint again"] end
graph LR subgraph "useLayoutEffect" L_R["Render"] --> L_D["DOM Update"] --> L_L["useLayoutEffect<br/>(阻塞 Paint)"] L_L --> L_R2["Re-render (若有 setState)"] --> L_D2["DOM Update"] --> L_P["Paint"] end
| useEffect | useLayoutEffect | |
|---|---|---|
| 执行时机 | 浏览器绘制之后,异步 | DOM 更新之后,绘制之前,同步 |
| 修改 state | 触发二次绘制(用户可见闪烁) | 在绘制前完成,用户看到最终结果 |
| 副作用 | 不阻塞渲染(推荐) | 阻塞渲染(慎用) |
| 主要用途 | 数据请求、订阅、日志 | DOM 测量、根据 DOM 调整样式 |
5.5 用 useRef + useEffect 实现 usePrevious
利用”useEffect 在 return 之后执行”的时序:
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value; // DOM 更新后才执行,此时渲染已完成
}, [value]);
return ref.current; // 返回的是上一次的值
}6. 性能机制
6.1 跳过重渲染的层级
graph TD A[父组件渲染] --> B{React.memo?} B -->|"props 浅比较未变"| C["跳过重渲染 ✓"] B -->|"props 变化或无 memo"| D["继续渲染"] D --> E{子组件 memo?} E -->|"未变"| F["跳过 ✗(无效)<br/>因为父组件渲染时<br/>已创建了新的 React Element<br/>props 引用已经变了"] F --> G["→ 需用 useMemo 包裹 JSX"]
6.2 各优化手段的本质
| 手段 | 作用层面 | 原理 |
|---|---|---|
React.memo | 跳过组件执行 | props 浅比较,不变则跳过 |
useMemo | 跳过计算 | deps 浅比较,不变则返回缓存值 |
useCallback | 跳过函数重建 | deps 浅比较,不变则返回同一函数引用 |
useTransition | 降级优先级 | 标记为非紧急更新,可被用户输入打断 |
useDeferredValue | 降级值更新 | 延迟某个值的更新,等待空闲 |
| React Compiler (19+) | 编译时自动优化 | 自动插入 memo/useMemo/useCallback,无需手动 |
6.3 key 的双重作用
| 作用 | 机制 |
|---|---|
| 列表 diff | 标识同一列表中的元素,追踪增删移动 |
| 强制重新挂载 | key 变了 → React 销毁旧组件,创建新组件(state 重置) |
// 利用 key 变化强制 unmount + mount
<ErrorBoundary key={resetToken}>
<ChildComponent />
</ErrorBoundary>7. React 19 核心变化
7.1 新增 Hook
| Hook | 解决什么问题 |
|---|---|
useActionState | 管理 form action 的执行状态和结果,内部基于 useReducer |
useOptimistic | 乐观更新:异步操作完成前立即更新 UI,完成后自动回滚或确认 |
useEffectEvent | 在 Effect 中读取最新 props/state 但不重新触发 Effect |
useFormStatus | (react-dom) 获取表单提交状态,自动处理 pending/loading |
7.2 Actions 机制
React 19 的核心理念变化:将表单交互从”手动的 onSubmit + setState + pending 管理”变成框架原生支持的声明式模式。
// React 18:手动管理一切
const [isPending, setIsPending] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsPending(true);
await submit(data);
setIsPending(false);
};
// React 19:Actions 原生支持
const [state, formAction, isPending] = useActionState(
submitAction,
initialState,
);
// <form action={formAction}> — React 自动管理 pending 状态7.3 use API
在渲染期间同步读取 Promise 或 Context,配合 Suspense:
// 在函数组件中直接 await(需要 Suspense 包裹)
const data = use(fetchPromise); // Promise resolve 前 Suspense 显示 fallback
const value = use(SomeContext); // 等价于 useContext,但可在条件/循环中使用之前怎么处理?
useEffect+useState:手动维护 loading/error/data 三个状态,手动处理竞态(AbortController),手动管理 cleanup。use把”你主动管理异步生命周期”变成”你声明依赖,React 运行时接管”——组件仍是同步函数,Promise 未就绪时 throw 给 Suspense 接住,就绪后重跑组件直接拿结果。消灭的是样板代码,不是异步本身。
7.4 新增组件
| 组件 | 用途 |
|---|---|
<ViewTransition> | 基于浏览器 View Transition API 的页面过渡动画 |
<Activity> | 控制组件活动状态(visible/hidden) |
7.5 React Compiler
编译时自动记忆化。"use memo" 指令标记文件启用自动优化,不再需要手动 useMemo/useCallback/React.memo。
Tip
新项目直接用,老项目慎重。
7.6 完整 Hook 清单(React 19,共 19 个)
| 分类 | Hook |
|---|---|
| State | useState, useReducer |
| Context | useContext |
| Ref | useRef, useImperativeHandle |
| Effect | useEffect, useLayoutEffect, useInsertionEffect, useEffectEvent |
| Performance | useMemo, useCallback, useTransition, useDeferredValue |
| Other | useDebugValue, useId, useSyncExternalStore, useActionState, useOptimistic |
| React DOM | useFormStatus |
| New API | use (不是 Hook,但类似) |
8. 重要”逃生舱”
8.1 Ref — 跨渲染的可变引用
interface MutableRefObject<T> {
current: T;
}useRef(initial)创建一个{ current: initial }对象,存入 Hook 链表- 后续渲染返回同一个对象引用
- 修改
.current不触发重渲染(这点与 state 不同) - 典型用途:持有 DOM 节点、保存不需要触发渲染的值(如定时器 ID、previous value)
为什么不能用一个普通对象或模块级变量? 本质是归不归 React 管理。渲染可能被并发中断并废弃(Suspense、transition),模块级变量被污染后无法回滚;组件卸载后模块级变量需手动清理;多个组件实例共享一个模块级变量需额外管理 Map。
useRef存在 Fiber 上,自动获得实例隔离、渲染回滚、卸载回收能力。
8.2 forwardRef + useImperativeHandle
父组件获取子组件 DOM 或暴露的方法:
// 子组件
const Son = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
reset: () => inputRef.current?.reset(),
}));
return <input ref={inputRef} />;
});
// 父组件
const ref = useRef(null);
<Son ref={ref} />
ref.current?.focus(); // 只暴露了 focus 和 reset,无法直接操作 DOM8.3 Error Boundary
React 对声明式组件树的异常处理机制,本质是 try-catch 的组件树版本。组件以声明方式使用(<Child />),无法用 try-catch 包裹,因此用声明式的包裹组件来捕获子树渲染崩溃,显示降级 UI。
必须是 class 组件。 依赖 getDerivedStateFromError() 和 componentDidCatch() 两个生命周期方法,函数组件不支持。React 19 仍未改变,官方无 useErrorBoundary Hook 计划。社区标准替代:react-error-boundary,内部 class 组件,对外提供函数式 API。
ErrorBoundary(class 组件)
├─ getDerivedStateFromError() — 渲染前捕获,更新 state 显示降级 UI
└─ componentDidCatch() — 渲染后捕获,记录日志/上报
无法捕获:事件处理器中(不在 render 时触发)、异步代码(setTimeout/Promise)、SSR、自身的异常。
重置方式:改变 ErrorBoundary 的 key 使其重新挂载,或用 getDerivedStateFromProps 屏蔽错误。
8.4 lazy + Suspense
lazy 将组件推迟到首次渲染时才加载(Code Splitting),必须配合 Suspense。
const LazyComp = lazy(() => import("./HeavyComponent"));
// <Suspense fallback={<Loading />}><LazyComp /></Suspense>原理和 use(promise) 一致: lazy 返回一个特殊的组件类型。第一次渲染时模块未加载 → React 尝试渲染 → lazy 内部 throw 加载模块的 Promise → Suspense 接住 → 显示 fallback。模块加载完成后重新渲染 → lazy 返回已缓存的组件 → 正常渲染。
Webpack/Vite 将 import() 的模块单独打包,按需请求。Suspense 的位置决定了 loading 的覆盖范围——放在叶子组件附近则仅该组件显示 fallback,放在路由顶层则整个页面切换。
9. React Router v7 内核
当前最新版本:v7.16(2026-05)
9.1 三种使用模式
graph LR subgraph "Declarative" D["<BrowserRouter> + <Routes> + <Route>"] end subgraph "Data" DA["createBrowserRouter() + <RouterProvider>"] end subgraph "Framework(完整框架)" F["routes.ts + Vite 插件 + SSR/SSG/SPA"] end D -->|"加强"| DA DA -->|"全栈"| F
三者不是互斥方案,而是能力递增:
| Declarative | Data | Framework | |
|---|---|---|---|
| 路由定义方式 | JSX(<Route>) | JS 对象(RouteObject[]) | 文件映射(routes.ts) |
| 数据加载 | 组件自己管(useEffect) | loader / action 挂在路由上 | 同上,增强 |
| 数据归属 | 组件级别 | 路由级别(数据在渲染前就绪) | 路由级别 |
| 渲染位置 | 仅客户端 | 仅客户端 | 客户端 / 服务端 / 静态导出 |
| 构建工具 | 任意 | 任意 | 强制 Vite |
| 服务端能力 | 无 | 无 | Cookies、Session、Middleware |
核心区别:Declarative 模式下,数据加载是组件自己的事,路由只管”匹配 → 渲染”。Data 模式把数据加载提升到路由层面——路由匹配后、渲染前就请求数据,组件渲染时数据已就绪,不再需要组件内 useEffect + loading state。Framework 模式则进一步把同一套路由配置变成全栈能力——同样的 routes.ts,可以输出纯 SPA、也可以输出 SSR/SSG。
Framework 模式的本质是多了一个内置服务端。 React Router v7 Framework = Remix v2 合并。Vite 插件同时产出两套 bundle:client bundle 给浏览器,server bundle 给 React Router 自己的 server(loader 执行、SSR、cookies/session 等)。不是 React 变全栈了,是 React Router 替你启动了一个服务端运行时,这个 server 里跑的还是你写的同一套组件代码。
Note
与 Vue Router 类比:React 本身不含路由,React Router 是独立库。它没有 Vue Router 的
beforeEnter、beforeEach守卫概念,但 loader 就是前置守卫——在路由匹配后、组件渲染前执行,既能拉数据,也能做权限拦截。throw redirect()等价于 Vue 的导航守卫中next('/login')。区别在于 Vue 将”数据请求”和”权限拦截”拆成两个概念,React Router 统一塞进了 loader。
9.2 路由匹配核心
// 匹配优先级:static > dynamic > *
// 例:path='/12345' 的匹配顺序
// 1. 先找 static: path='12345'
// 2. 再找 dynamic: path=':userId'
// 3. 最后 star: path='*'匹配结果是 RouteMatch[] 数组,逐级渲染父 → 子。每个 match 的组件通过 <Outlet /> 渲染下一个 match。
9.3 导航本质
sequenceDiagram participant B as Browser participant H as History Stack participant R as Router participant C as Components B->>H: pushState/replaceState/popState H->>R: location 变化 R->>R: matchRoutes(location) R->>C: 重新渲染匹配到的组件树
PUSH:新增历史记录(Link 点击)REPLACE:替换当前记录(重定向)POP:浏览器前进/后退(back/forward)
注意:导航类型(POP/PUSH/REPLACE)不直接等于”前进/后退”。用户 back 再 forward,视觉上是前进,但类型是 POP(因为是从 history stack 弹出的)。
9.4 v7 vs v6 关键变化
| 维度 | v6 | v7 |
|---|---|---|
| 包名 | react-router-dom | 统一为 react-router |
| 定位 | 纯客户端路由库 | 从路由到全栈框架 |
| 全栈能力 | 无 | SSR/SSG/SPA、Cookies、Session、Middleware |
| 类型安全 | 无原生支持 | 内置 Route 类型推断 |
| 代码分割 | 手动 lazy | 框架模式自动 |
| 服务端 API | 无 | Cookies、Session、Headers |
9.5 核心 Hooks 速查
| 类别 | Hook | 用途 |
|---|---|---|
| 数据 | useLoaderData() | 获取路由 loader 返回的数据 |
| 数据 | useActionData() | 获取 form action 返回的数据 |
| 数据 | useRouteError() | 获取 ErrorBoundary 捕获的错误 |
| 导航 | useNavigate() | 编程式导航 |
| 导航 | useNavigation() | 当前导航状态(idle/loading/submitting) |
| URL | useParams() | 路径参数 /:id |
| URL | useSearchParams() | query string 读写 |
| URL | useLocation() | 当前 location 对象 |
| 渲染 | useOutlet() | 子路由元素 |
| 表单 | useSubmit() | 编程式表单提交 |
| 请求 | useFetcher() | 不触发导航的数据请求 |
| 匹配 | useMatch(pattern) | 是否匹配给定的路径模式 |
10. 关键心智模型总结
10.1 三类函数
graph TD F[JavaScript 函数] F -->|"返回 JSX<br/>被 JSX 标签调用<br/>有独立 Fiber"| C[组件] F -->|"调用内置 Hook<br/>被组件函数体调用<br/>借用调用者的 Fiber"| H[Hook] F -->|"不调 Hook<br/>不返 JSX<br/>无 Fiber 关联"| U[普通工具函数]
三者没有语言层面的区别——同一个函数,怎么调用它决定了它是什么。use 前缀和大写开头只是给人看和 lint 用的。
10.2 理解 Hook 的一句话
Hook 函数本身无状态,状态永远附着在 Fiber 上。 组件每次执行都是全新调用,但 Hook 通过调用顺序在 Fiber 链表上找到持久化数据。自定义 Hook 是宏展开,展开后链表仍是扁平的。
10.3 React 运行时 = 数据在外、逻辑在内
你的代码(函数) React 运行时(数据)
───────────────── ────────────────────
useState(0) ──────→ Fiber._hooks[0] = { state: ... }
useEffect(...) ──────→ Fiber._hooks[1] = { effect: ... }
useRef(null) ──────→ Fiber._hooks[2] = { current: ... }
函数执行时:从右往左"拉数据"
setState 时:从左往右"推更新"
当你在函数里写 const [count, setCount] = useState(0),你实际上是在说:“帮我从外部持久化存储中读取名为’当前调用序号’的那个值;如果没有,用 0 初始化。”