React Router:History API、核心原理与路由模式实现

3 阅读9分钟

一、浏览器 History API 详解

1. History 对象核心功能

// 访问历史记录信息
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)

history.pushState(
  {
    page: "dashboard",
    filters: { category: "tech", date: "2023" },
    scrollY: 1200
  }, 
  "Dashboard", 
  "/dashboard"
);
  • 特性

    • 最大支持 2-10MB 数据(浏览器差异)
    • 使用结构化克隆算法序列化
    • 页面重新加载后仍然可用
    • 不可存储函数、DOM 元素等非序列化对象

4. popstate 事件

window.addEventListener('popstate', (event) => {
  console.log('导航到:', location.pathname);
  console.log('状态对象:', event.state);
});
  • 触发条件

    • 用户点击浏览器前进/后退按钮
    • 调用 history.back()/forward()/go()
    • 不触发:history.pushState() 和 replaceState()

二、React Router 与 History 库关系

1. 依赖关系

image.png

2. History 库核心作用

React Router 是一个用于构建单页应用的路由管理库,而其内部正是依赖于 history 库 来处理与浏览器 History API 的交互。

  • history 库 提供了统一的 API 封装,屏蔽了浏览器各版本之间的差异,并支持两种路由模式:

    • 基于 HTML5 History API 的 BrowserRouter
    • 基于 URL hash 的 HashRouter
  • React Router 在使用 BrowserRouterHashRouter 时,内部会创建一个 history 对象,并通过该对象进行导航(比如调用 push(), replace() 等方法)。同时 React Router 通过监听 history 对象的变化(利用 history.listen())来触发自已内部状态的更新,进而重新匹配路由和更新视图。

3. 关键接口实现

// 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 的核心工作流程主要分为以下几个步骤:

  1. 导航操作 用户交互(点击 <Link> 组件)或编程式调用 history.push()/replace(),会更新 URL,这取决于使用 BrowserRouter(History API)或 HashRouter(修改 location.hash)。

  2. 历史记录更新 & 通知 当 URL 变化时,history 库内部通过事件订阅(发布-订阅模式) 通知所有注册的监听器。(参见下文详细介绍 history.listen() 机制)

  3. 状态更新触发视图重渲染 React Router 内部接收到新的 location 后,通过调用 setState({ location, action }) 更新内部状态,利用 React 的组件生命周期重新渲染和匹配路由。

  4. 路由匹配 更新后的 location 会通过 Routes 组件解析,根据预定义的配置匹配最佳路由,选择需要渲染的组件树。 路由匹配通常包括:

    • 扁平化所有路由配置
    • 根据路径特异性排序
    • 选择最佳匹配路由,支持嵌套路由(通过 Outlet 组件)
  5. 组件渲染 & 优化 最后只有与新 URL 匹配的组件会被渲染更新,同时未变化的组件能够保持状态,避免不必要的重渲染。

四、history.listen 监听机制详解

1. 核心原理:发布-订阅模式

history 库内部维护一个监听器列表,利用发布-订阅模式来实现状态变化的监听。下面是一个简化实现:

// 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. 针对不同路由模式的事件触发机制

事件源BrowserRouterHashRouter
主动导航history.push() 内部调用通知监听 hashchange 事件
浏览器导航监听 popstate 事件监听 hashchange 事件
替换操作history.replace() 内部调用通知监听 hashchange 事件

1. 主动导航

BrowserRouter 下(使用 History API)
  1. 开发者调用 history.push('/somePath')
  2. 内部执行:
    • 调用 window.history.pushState({}, '', '/somePath') 以更新 URL。
    • 不会触发浏览器的 popstate 事件(因为 pushState 本身不触发 popstate)。
  3. 内部的 push() 方法会创建新的 location 对象,并立即调用内部的 notifyListeners(location, 'PUSH')
  4. 所有通过 history.listen() 注册的监听器(包括 React Router 的监听器)被依次调用,并通过回调触发 setState() 更新,从而引起组件重新渲染与路由匹配。
HashRouter 下(基于 hash 值)
  1. 开发者调用 history.push('/somePath')
  2. 内部执行:
    • 修改 window.location.hash#/somePath
    • 此操作直接改变了浏览器地址栏的 hash 部分。
  3. 由于 hash 的变化,浏览器会自动触发 hashchange 事件。
  4. 捕获hashchange,构造新location 对象,调用 notifyListeners(location, 'PUSH')
  5. 然后,React Router 的注册监听器接收到更新,执行 setState(),从而触发组件的更新与路由匹配。

2. 浏览器导航

BrowserRouter
  1. 用户点击浏览器的“前进/后退”按钮,浏览器更新 URL 并触发 popstate 事件。
  2. Router 内部已经注册了对 popstate 事件的监听。
  3. 在事件处理器中,调用类似于 getCurrentLocation() 获取最新的 URL 信息,构造新的 location 对象。
  4. 调用内部的 notifyListeners(location, 'POP') 通知所有注册监听器。
  5. React Router 监听到更新,调用 setState() 触发组件的重渲染与路由重新匹配。
HashRouter
  1. 用户点击浏览器的“前进/后退”按钮,浏览器改变 window.location.hash 并触发 hashchange 事件。
  2. HashRouter 内部对 hashchange 事件的监听函数被调用。
  3. 在事件处理器中,从新的 hash 中创建新的 location 对象。
  4. 内部调用 notifyListeners(location, 'POP')
  5. React Router 接收到更新后,调用 setState() 更新状态,并重新匹配路由与更新组件。

3. 替换操作

替换操作使用 history.replace(),其触发机制与主动导航类似,不过主要区别在于不会生成一个新的历史记录条目,而是覆盖当前记录。在两种路由模式下的逻辑类似:

BrowserRouter
  1. 调用 history.replace('/anotherPath')
  2. 内部调用 window.history.replaceState({}, '', '/anotherPath') 修改当前历史记录条目。
  3. 创建新的 location 对象,然后调用 notifyListeners(location, 'REPLACE')
  4. React Router 的监听函数接收到更新,调用 setState(),触发 UI 更新。
HashRouter
  1. 调用 history.replace('/anotherPath')
  2. 内部修改 window.location.hash(或采用类似替换操作的方法),由此更新 hash。
  3. 同样触发内部机制,创建新的 location 对象,并调用 notifyListeners(location, 'REPLACE')
  4. 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 触发更新

image.png

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

image.png

六、setState 触发后的更新流程

1. 状态更新阶段

React Router 内部维护着一个 state 对象,记录当前的 locationaction(例如 PUSH、POP、REPLACE): const [state, setState] = useState({ location: initialLocation, action: initialAction });

// 当新的导航操作发生时,调用:
setState({
  location: newLocation, // 更新后的 location 对象
  action: 'PUSH' // 或 'POP''REPLACE'
});

2. 组件更新流程

setState() 被调用后,整个路由组件树将依次经历以下过程:

  1. Router 组件重新渲染

    • 内部状态更新后,通过 React Context 将新的 location 分发给所有子组件。
  2. Routes 组件重新匹配路由

    function Routes({ children }) {
      const { location } = useLocation();
      
      // 根据当前 location 匹配路由
      const matches = matchRoutes(children, location);
      
      // 根据匹配结果渲染对应的组件树
      return _renderMatches(matches);
    }
    
  3. 路由匹配算法

    • 扁平化路由配置
    • 按特异性排序(例如:具体路径优先于动态路径)
    • 返回最佳匹配集合,然后使用 <Outlet> 渲染嵌套路由
  4. 组件渲染优化

    • 仅重新渲染受影响的组件
    • 保留未变路径组件的状态,避免重渲染

3. 相关 Hook 的更新

Hook更新触发条件使用场景
useLocation()location 变化时获取当前路径信息
useParams()动态参数变化时获取 URL 参数
useNavigate()独立于状态更新进行编程式导航
useMatch()路径匹配变化时检查当前路径是否匹配指定模式

七、BrowserRouter 与 HashRouter 对比

1. 实现机制差异

特性BrowserRouterHashRouter
URL 格式example.com/pathexample.com/#/path
依赖 APIHTML5 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 支持嵌套匹配

九、总结核心流程

  1. 导航触发

    • 用户交互(点击 <Link>)或编程调用(history.push()
    • 浏览器前进/后退
  2. URL 变更

    • BrowserRouter:调用 window.history.pushState() 更新 URL
    • HashRouter:赋值 window.location.hash
  3. 事件通知

    • history 库内通过发布-订阅机制,调用 notifyListeners(location, action)
    • React Router 内部监听函数(通过 history.listen())被调用
  4. 状态更新 & 路由匹配

    • 调用 setState({ location, action }) 更新内部状态
    • Router 组件重新渲染
    • Routes 根据新 location 执行匹配算法,选择最佳路由配置
  5. 组件渲染

    • 渲染匹配的组件树
    • 使用 Route Hooks(如 useLocation, useParams)响应变化
    • 未变化部分保持不被重渲,优化整体性能