一、浏览器 History API 详解
1. History 对象核心功能
arduino
// 访问历史记录信息
console.log(history.length); // 历史记录条目数
console.log(history.state); // 当前历史记录状态对象
2. 核心方法
方法 | 作用 | 特点 |
---|---|---|
history.pushState() |
添加新历史记录条目 | 不刷新页面,不触发事件 |
history.replaceState() |
替换当前历史记录条目 | 不增加历史记录长度 |
history.go() |
在历史记录中移动 | 触发 popstate 事件 |
history.back() |
后退到上一页 | 相当于 history.go(-1) |
history.forward() |
前进到下一页 | 相当于 history.go(1) |
3. 状态对象(State Object)
php
history.pushState(
{
page: "dashboard",
filters: { category: "tech", date: "2023" },
scrollY: 1200
},
"Dashboard",
"/dashboard"
);
-
特性:
- 最大支持 2-10MB 数据(浏览器差异)
- 使用结构化克隆算法序列化
- 页面重新加载后仍然可用
- 不可存储函数、DOM 元素等非序列化对象
4. popstate 事件
javascript
window.addEventListener('popstate', (event) => {
console.log('导航到:', location.pathname);
console.log('状态对象:', event.state);
});
-
触发条件:
- 用户点击浏览器前进/后退按钮
- 调用 history.back()/forward()/go()
- 不触发:history.pushState() 和 replaceState()
二、React Router 与 History 库关系
1. 依赖关系

2. History 库核心作用
React Router 是一个用于构建单页应用的路由管理库,而其内部正是依赖于 history 库 来处理与浏览器 History API 的交互。
-
history 库 提供了统一的 API 封装,屏蔽了浏览器各版本之间的差异,并支持两种路由模式:
- 基于 HTML5 History API 的 BrowserRouter
- 基于 URL hash 的 HashRouter
-
React Router 在使用
BrowserRouter
或HashRouter
时,内部会创建一个 history 对象,并通过该对象进行导航(比如调用push()
,replace()
等方法)。同时 React Router 通过监听 history 对象的变化(利用history.listen()
)来触发自已内部状态的更新,进而重新匹配路由和更新视图。
3. 关键接口实现
javascript
// History 库监听器管理
const listeners = new Set();
function listen(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
}
function notifyListeners(location, action) {
listeners.forEach(listener => listener(location, action));
}
三、React Router 核心原理
React Router 的核心工作流程主要分为以下几个步骤:
-
导航操作 用户交互(点击
<Link>
组件)或编程式调用history.push()
/replace()
,会更新 URL,这取决于使用 BrowserRouter(History API)或 HashRouter(修改location.hash
)。 -
历史记录更新 & 通知 当 URL 变化时,history 库内部通过事件订阅(发布-订阅模式) 通知所有注册的监听器。(参见下文详细介绍
history.listen()
机制) -
状态更新触发视图重渲染 React Router 内部接收到新的
location
后,通过调用setState({ location, action })
更新内部状态,利用 React 的组件生命周期重新渲染和匹配路由。 -
路由匹配 更新后的
location
会通过Routes
组件解析,根据预定义的配置匹配最佳路由,选择需要渲染的组件树。 路由匹配通常包括:- 扁平化所有路由配置
- 根据路径特异性排序
- 选择最佳匹配路由,支持嵌套路由(通过 Outlet 组件)
-
组件渲染 & 优化 最后只有与新 URL 匹配的组件会被渲染更新,同时未变化的组件能够保持状态,避免不必要的重渲染。
四、history.listen 监听机制详解
1. 核心原理:发布-订阅模式
history 库内部维护一个监听器列表,利用发布-订阅模式来实现状态变化的监听。下面是一个简化实现:
javascript
// history 库内部简化实现
const listeners = new Set(); // 存储所有监听函数
function createBrowserHistory() {
// 监听器管理
function listen(listener) {
listeners.add(listener);
return () => listeners.delete(listener); // 返回取消监听函数
}
// 通知所有监听器
function notifyListeners(location, action) {
listeners.forEach(listener => listener(location, action));
}
// 处理导航操作
function push(path) {
// 1. 更新 URL
window.history.pushState({}, '', path);
// 2. 创建新 location 对象(封装当前 URL 信息)
const location = createLocation(path);
// 3. 通知所有监听器
notifyListeners(location, 'PUSH');
}
// 同理还支持 replace() 等
return { listen, push };
}
2. 针对不同路由模式的事件触发机制
事件源 | BrowserRouter | HashRouter |
---|---|---|
主动导航 | history.push() 内部调用通知 |
监听 hashchange 事件 |
浏览器导航 | 监听 popstate 事件 |
监听 hashchange 事件 |
替换操作 | history.replace() 内部调用通知 |
监听 hashchange 事件 |
1. 主动导航
BrowserRouter 下(使用 History API)
- 开发者调用
history.push('/somePath')
- 内部执行:
- 调用
window.history.pushState({}, '', '/somePath')
以更新 URL。 - 不会触发浏览器的
popstate
事件(因为 pushState 本身不触发 popstate)。
- 调用
- 内部的
push()
方法会创建新的 location 对象,并立即调用 内部的notifyListeners(location, 'PUSH')
。 - 所有通过
history.listen()
注册的监听器(包括 React Router 的监听器)被依次调用,并通过回调触发setState()
更新,从而引起组件重新渲染与路由匹配。
HashRouter 下(基于 hash 值)
- 开发者调用
history.push('/somePath')
- 内部执行:
- 修改
window.location.hash
为#/somePath
。 - 此操作直接改变了浏览器地址栏的 hash 部分。
- 修改
- 由于 hash 的变化,浏览器会自动触发
hashchange
事件。 - 捕获
hashchange
,构造新location 对象,调用notifyListeners(location, 'PUSH')
。 - 然后,React Router 的注册监听器接收到更新,执行
setState()
,从而触发组件的更新与路由匹配。
2. 浏览器导航
BrowserRouter
- 用户点击浏览器的"前进/后退"按钮,浏览器更新 URL 并触发
popstate
事件。 - Router 内部已经注册了对
popstate
事件的监听。 - 在事件处理器中,调用类似于
getCurrentLocation()
获取最新的 URL 信息,构造新的 location 对象。 - 调用内部的
notifyListeners(location, 'POP')
通知所有注册监听器。 - React Router 监听到更新,调用
setState()
触发组件的重渲染与路由重新匹配。
HashRouter
- 用户点击浏览器的"前进/后退"按钮,浏览器改变
window.location.hash
并触发hashchange
事件。 - HashRouter 内部对
hashchange
事件的监听函数被调用。 - 在事件处理器中,从新的 hash 中创建新的 location 对象。
- 内部调用
notifyListeners(location, 'POP')
; - React Router 接收到更新后,调用
setState()
更新状态,并重新匹配路由与更新组件。
3. 替换操作
替换操作使用 history.replace()
,其触发机制与主动导航类似,不过主要区别在于不会生成一个新的历史记录条目,而是覆盖当前记录。在两种路由模式下的逻辑类似:
BrowserRouter
- 调用
history.replace('/anotherPath')
- 内部调用
window.history.replaceState({}, '', '/anotherPath')
修改当前历史记录条目。 - 创建新的 location 对象,然后调用
notifyListeners(location, 'REPLACE')
。 - React Router 的监听函数接收到更新,调用
setState()
,触发 UI 更新。
HashRouter
- 调用
history.replace('/anotherPath')
- 内部修改
window.location.hash
(或采用类似替换操作的方法),由此更新 hash。 - 同样触发内部机制,创建新的 location 对象,并调用
notifyListeners(location, 'REPLACE')
。 - React Router 接收到更新,通过
setState()
触发组件更新。
3. 具体监听流程
初始化监听:例如 React Router 在 Router 组件中设置监听: useEffect(() => { const unlisten = history.listen((location, action) => { // 核心:触发状态更新,进而更新路由对应的组件 setState({ location, action }); }); return unlisten; // 组件卸载时清理监听 }, []);
事件触发过程概述:
- 当 URL 变化时(无论是通过
push/replace
调用还是浏览器的前进后退),history 库内部都会调用notifyListeners(location, action)
。 - 所有注册的监听函数依次被调用,React Router 内部的监听函数接收到新 location 后调用
setState()
更新状态。
五、完整导航流程示例(BrowserRouter)
1. history.push 触发更新

2. 浏览器前进/后退触发更新

六、setState 触发后的更新流程
1. 状态更新阶段
React Router 内部维护着一个 state 对象,记录当前的 location
和 action
(例如 PUSH、POP、REPLACE): const [state, setState] = useState({ location: initialLocation, action: initialAction });
php
// 当新的导航操作发生时,调用:
setState({
location: newLocation, // 更新后的 location 对象
action: 'PUSH' // 或 'POP' 或 'REPLACE'
});
2. 组件更新流程
当 setState()
被调用后,整个路由组件树将依次经历以下过程:
-
Router 组件重新渲染
- 内部状态更新后,通过 React Context 将新的
location
分发给所有子组件。
- 内部状态更新后,通过 React Context 将新的
-
Routes 组件重新匹配路由
scssfunction Routes({ children }) { const { location } = useLocation(); // 根据当前 location 匹配路由 const matches = matchRoutes(children, location); // 根据匹配结果渲染对应的组件树 return _renderMatches(matches); }
-
路由匹配算法
- 扁平化路由配置
- 按特异性排序(例如:具体路径优先于动态路径)
- 返回最佳匹配集合,然后使用
<Outlet>
渲染嵌套路由
-
组件渲染优化
- 仅重新渲染受影响的组件
- 保留未变路径组件的状态,避免重渲染
3. 相关 Hook 的更新
Hook | 更新触发条件 | 使用场景 |
---|---|---|
useLocation() |
location 变化时 | 获取当前路径信息 |
useParams() |
动态参数变化时 | 获取 URL 参数 |
useNavigate() |
独立于状态更新 | 进行编程式导航 |
useMatch() |
路径匹配变化时 | 检查当前路径是否匹配指定模式 |
七、BrowserRouter 与 HashRouter 对比
1. 实现机制差异
特性 | BrowserRouter | HashRouter |
---|---|---|
URL 格式 | example.com/path |
example.com/#/path |
依赖 API | HTML5 History API(pushState) | window.location.hash |
服务器要求 | 需配置 SPA 支持(路由重定向) | 无需特殊配置 |
state 支持 | 支持完整 state 对象(大容量) | 受限,仅能通过 URL 传递少量信息 |
SEO 友好度 | 高 | 低 |
2. 事件处理差异
参考上文不同路由模式的事件触发机制
八、Routes 组件工作机制
1. 匹配算法流程
-
扁平化路由配置 将嵌套路由转换成一个平面数组,便于统一匹配。
-
路径排名计算 例如,通过计算动态参数数量来确定路径特异性: // 计算路径特异性分数,动态参数越多分数越低 function rankRoute(route) { return route.path.split('/') .filter(segment => segment.startsWith(':')) .length; }
-
最佳匹配选择 按照特异性排序,选择排名最高的匹配路由。
-
Outlet 渲染 对于嵌套路由,使用
<Outlet>
组件来呈现子路由。
2. 与 v5 版 Switch 组件对比
特性 | Switch (v5) | Routes (v6) |
---|---|---|
匹配逻辑 | 首个匹配 | 根据特异性选择最佳匹配 |
嵌套路由 | 不支持 | 原生支持 |
相对路径 | 不支持 | 支持 |
路由排序 | 需手动排序 | 按照特异性自动排序 |
多路由匹配 | 不支持 | 使用 Outlet 支持嵌套匹配 |
九、总结核心流程
-
导航触发
- 用户交互(点击
<Link>
)或编程调用(history.push()
) - 浏览器前进/后退
- 用户交互(点击
-
URL 变更
- BrowserRouter :调用
window.history.pushState()
更新 URL - HashRouter :赋值
window.location.hash
- BrowserRouter :调用
-
事件通知
- history 库内通过发布-订阅机制,调用
notifyListeners(location, action)
- React Router 内部监听函数(通过
history.listen()
)被调用
- history 库内通过发布-订阅机制,调用
-
状态更新 & 路由匹配
- 调用
setState({ location, action })
更新内部状态 - Router 组件重新渲染
- Routes 根据新 location 执行匹配算法,选择最佳路由配置
- 调用
-
组件渲染
- 渲染匹配的组件树
- 使用 Route Hooks(如 useLocation, useParams)响应变化
- 未变化部分保持不被重渲,优化整体性能