React Router路由原理深度解析
React Router 核心是 BrowserRouter 监听 URL 变化(History API 或 hashchange)、Routes 用评分算法匹配路径、RouteContext 与 Outlet 渲染匹配结果。 本质是 URL 与组件映射的声明式实现。
一句话概括
React Router 核心就三件事:BrowserRouter 监听 URL 变化(History API / hashchange),Routes 用评分算法匹配路径,RouteContext + Outlet 把匹配结果渲染到正确位置。精炼下来就是”URL ↔ 组件映射”的声明式实现。
核心知识点
1. BrowserRouter vs HashRouter 的本质区别
1
2
3
4
5
6
7
// BrowserRouter:基于 History API,URL 干净但需要服务端配合
window.history.pushState(null, '', '/about'); // URL变但不刷新
window.addEventListener('popstate', handler); // 监听前进后退
// HashRouter:基于 location.hash,兼容所有浏览器但 URL 带 #
window.location.hash = '/about'; // 触发 hashchange
window.addEventListener('hashchange', handler);
BrowserRouter 直接刷新 /about 会 404,因为浏览器向服务端发了请求。必须配 Nginx try_files $uri /index.html 把非静态资源的请求都回退到 index.html。
2. 路径评分算法(为什么 Routes 不需要关心顺序)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// React Router 的评分逻辑
function computeScore(path) {
let score = 0;
for (const seg of path.split('/').filter(Boolean)) {
if (seg.startsWith(':')) score += 1; // 动态段 :id
else if (seg === '*') score -= 1; // 通配符
else score += 10; // 静态段 users
}
return score;
}
// /users/list → 10+10=20 分
// /users/:id → 10+1=11 分
// /:a/:b → 1+1=2 分
// * → -1 分
// 匹配时选分数最高的,和声明顺序无关!
这是 v6 Routes 替代 v5 Switch 的关键。v5 是”匹配第一个符合的”(依赖顺序),v6 是”匹配分数最高的”(不依赖顺序)。
3. 三层 Context 架构
1
2
3
4
5
// React Router 内部的三层 Context
<NavigationContext> // 提供 navigate() 方法
<LocationContext> // 提供当前 location 和 action
<RouteContext> // 提供当前匹配结果和 outlet(子路由元素)
{children}
useNavigate → 读 NavigationContext;useLocation → 读 LocationContext;useParams / Outlet → 读 RouteContext。
4. Outlet 的嵌套渲染原理
1
2
3
4
5
6
7
8
9
10
// Outlet 就是一个 "路由占位符"
function Outlet() {
const routeContext = useContext(RouteContext);
return routeContext.outlet; // 当前匹配的下一级子元素
}
// Routes 用 reduceRight 从深层到浅层构建 Provider 嵌套
// 如果匹配到 /dashboard/settings:
// 最内层 Provider → outlet = <Settings/>
// 外层 Provider → outlet = <DashboardLayout>(其内部 <Outlet/> 渲染 <Settings/>)
这就是为什么嵌套路由的 UI 层级能直接对应文件目录结构——每个 <Route> 节点的 element 里放 <Outlet />,子路由自动填入。
5. 导航守卫
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
function Editor() {
const blocker = useBlocker(
({ nextLocation }) => hasUnsavedChanges
);
useEffect(() => {
if (blocker.state === 'blocked') {
if (confirm('有未保存的更改,确定离开?')) {
blocker.proceed();
} else {
blocker.reset();
}
}
}, [blocker]);
return <textarea onChange={...} />;
}
底层原理:navigate 时先检查 blocker,被拦截就暂停更新并设置 state='blocked',等用户决定。
其实你每天都在用
- SPA 页面切换不刷新:
<Link to="/about">点击后 URL 变了、内容变了,但浏览器没发 HTTP 请求——背后是pushState+ React 重渲染 - 直接访问深层路由:刷新
/dashboard/analytics页面正常显示——服务端try_files把请求交给 index.html,前端 Router 初始化时从当前 URL 解析路径匹配组件 - 嵌套布局复用:
<Route path="dashboard" element={<Layout/>}><Route path="settings" element={<Settings/>}/></Route>→ 切换子路由时 Layout 组件不销毁,只换 Outlet 内容 - 路由传参:
/user/:id→useParams()拿到{ id: '42' },比 query string 更 RESTful
常见误解(FAQ)
❌ 误区:「HashRouter 也能服务端渲染」 不能。# 后面的内容不会发送给服务端,服务端永远只看到根路径。SSR 必须用 BrowserRouter。
❌ 误区:「Link 组件和 a 标签完全一样」 不一样。Link 拦截点击事件,调用
navigate()而非window.location.href,不触发完整页面刷新。a 标签会导致白屏重载。❌ 误区:「v6 的 Routes 和 v5 的 Switch 只是改名」 不是。最大的变化是路径匹配从”声明顺序”改为”评分制”,同时嵌套路由不再需要手动在父组件中写
<Route>和<Switch>。❌ 误区:「路由变化就是组件卸载再挂载」 同级路由切换时是新组件挂载,但嵌套路由中父布局组件不会卸载——只有
<Outlet/>内的内容会切换。
一句话总结
前端路由的本质 = URL 变化检测 + 路径匹配引擎 + 组件渲染占位——React Router 把这三件事全封装进了三级 Context,让你用写 JSX 的方式描述整个应用的页面结构。