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跳过函数重建
useContextContext 引用订阅上下文变化
三类目的:
  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函数参数传递父 → 子(显式)运行时调用
ContextFiber.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
useEffectuseLayoutEffect
执行时机浏览器绘制之后,异步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
StateuseState, useReducer
ContextuseContext
RefuseRef, useImperativeHandle
EffectuseEffect, useLayoutEffect, useInsertionEffect, useEffectEvent
PerformanceuseMemo, useCallback, useTransition, useDeferredValue
OtheruseDebugValue, useId, useSyncExternalStore, useActionState, useOptimistic
React DOMuseFormStatus
New APIuse (不是 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,无法直接操作 DOM

8.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

三者不是互斥方案,而是能力递增

DeclarativeDataFramework
路由定义方式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 的 beforeEnterbeforeEach 守卫概念,但 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 关键变化

维度v6v7
包名react-router-dom统一为 react-router
定位纯客户端路由库从路由到全栈框架
全栈能力SSR/SSG/SPA、Cookies、Session、Middleware
类型安全无原生支持内置 Route 类型推断
代码分割手动 lazy框架模式自动
服务端 APICookies、Session、Headers

9.5 核心 Hooks 速查

类别Hook用途
数据useLoaderData()获取路由 loader 返回的数据
数据useActionData()获取 form action 返回的数据
数据useRouteError()获取 ErrorBoundary 捕获的错误
导航useNavigate()编程式导航
导航useNavigation()当前导航状态(idle/loading/submitting)
URLuseParams()路径参数 /:id
URLuseSearchParams()query string 读写
URLuseLocation()当前 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 初始化。”