返回首页

Vue Router如何重塑导航

布莱克2026-04-08 15:43已编辑
Tip:文章封面与内容无关,作者旅游时拍摄,因为没什么值得把四季都错过!

传统跳转(全量刷新)

当你访问 a.com/page1 跳到 a.com/page2 时:

  1. 浏览器销毁当前页面的所有变量(Vue 实例、未提交的 Vuex/Pinia 数据、定时器)。
  2. 向服务器请求新的 HTML。
  3. 重新加载 CSS、重新下载并执行 JS(即便两个页面的 JS 是一模一样的)。
  4. 结果: 性能浪费,且无法维持跨页面的动画或复杂的应用状态。

Vue Router(组件映射)

Vue Router 的出现,就是为了在 “不刷新页面” 的前提下,模拟出 “换页” 的效果。

它建立了一张映射表
| URL 路径 | 对应的 Vue 组件 |
| :--- | :--- |
| /home | Home.vue |
| /user/:id | User.vue |

它的工作流程是:

  1. 感知变化:通过监听 popstate 或 hashchange 知道 URL 变了。
  2. 查找映射:在内存里的路由表查到:“哦,现在该看 User 组件了”。
  3. 局部替换:Vue Router 通知 Vue 引擎:“把页面里那个 <router-view> 位置的组件切成 User 吧”

实现了三个维度的“桥接”

  1. URL 与 组件的映射:让每一个 URL 都有一个对应的 UI 界面。
  2. History 与 内存状态的同步:在不刷新页面的前提下,通过劫持浏览器历史记录,让“后退/前进”按钮在单页应用中依然有效。
  3. 导航生命周期的管理:提供了一套拦截机制(钩子函数),让开发者能在页面切换的“缝隙”里处理权限、埋点或异步数据加载

第一层:History 管理器(抹平差异)

它封装了浏览器的原生 API。

  • Hash 模式:监听 hashchange。
  • History 模式:监听 popstate,并封装 pushState/replaceState。
  • 作用:对外提供统一的 push、replace 接口,无论底层用哪种模式,上层调用逻辑一致。

第二层:Matcher 匹配器(建立映射)

这是路由器的“大脑”。

  • 路由表转换:将你写的嵌套对象转换成扁平化的索引表。

   如果保持原始的嵌套树结构,每次 URL 变化都要进行一次递归搜索,在大型应用中(比如包含数百个路由),性能开销将不可接受

   当你执行 createRouter 时,内部会调用一个名为 createRouterMatcher 的函数。它会遍历你定义的嵌套 routes 数组,递归地生成一个扁平化的结构

   为了实现 $O(1)$ 或极高效率的查询,它会维护三张核心表:(Vue Router4)

       matcherMap:以 name 为 Key 的映射表。

       pathMatchers:一个包含所有路由记录的数组(用于路径正则匹配)。

       matchers:所有路由记录的集合

扁平化前:

const routes = [
  {
    path: '/algorithm',
    component: Parent,
    children: [
      { name: 'Detail', path: 'detail/:id', component: Detail }
    ]
  }
];

扁平化后:

// 扁平化后的 Matchers 数组
[
  {
    path: '/algorithm',
    record: { component: Parent, children: [...] },
    re: /^\/algorithm\/?$/i, // 自动生成的正则表达式
    score: [10, 0], // 用于排序的权重分
  },
  {
    path: '/algorithm/detail/:id',
    name: 'Detail',
    record: { component: Detail },
    re: /^\/algorithm\/detail\/([^/]+?)\/?$/i,
    score: [10, 10, 5], // 动态参数权重略低于静态路径
    parent: { ... } // 依然保留对父级的引用,以便渲染嵌套视图
  }
]
  • 正则匹配:当你给出一个路径时,它通过 pathToRegexp 快速定位到对应的 RouteRecord。

第三层:响应式桥接(驱动渲染)

这是 Vue Router 区别于其他框架路由的关键。

  • 它在内部维护一个响应式变量(currentRoute)。
  • 当 History 模块检测到 URL 变化 -> Matcher 匹配到新组件 -> 修改 currentRoute。
  • 此时,依赖该变量的 <router-view> 会自动触发 Vue 的 render 函数,完成组件的物理替换。

执行流程

比如router.push(`/algorithm/detail/${row._id}`)执行后,内部经历了哪些步骤

第一阶段:标准化与初筛(History 模块)

当你调用 router.push,首先接待你的是 History 记录管理器

  1. 参数规格化:Router 首先判断你传的是字符串还是对象。如果是字符串(如上述示例),它会立刻将其解析为一个 Location 对象。
  2. 预判拦截:它会检查新路径是否与当前路径完全一致。如果完全一样(且没有强制刷新的逻辑),它可能会直接拦截,避免重复跳转产生的冗余性能开销。


第二阶段:寻图定位(Matcher 匹配器)

这是最硬核的一步。Router 需要弄清楚:这个字符串路径到底对应谁?

  1. 正则提取:Matcher 拿着 /algorithm/detail/123 去路由表里匹配。
  2. 得分计算 (Ranking):它发现你的路由配置里有一个 { path: '/algorithm/detail/:id', name: 'AlgDetail', component: Detail }。Matcher 内部的 pathToRegexp 跑了一遍,确认匹配成功,并从 URL 中提取出 params: { id: '123' }。
  3. 生成匹配记录 (Matched Records):如果是嵌套路由,Matcher 会生成一个数组(如 [ParentRecord, ChildRecord])。输出:一个 Route 对象(即我们平时用的 $route 的雏形),里面包含了完整的 path, params, query 和 meta。


第三阶段:过五关斩六将(导航守卫流水线)

在 URL 真正改变之前,必须通过一系列的“安检”。Vue Router 4 将这些钩子函数串联成了一个 Promise 链

  1. 失活钩子:调用当前页面组件的 beforeRouteLeave。
  2. 全局前置:执行 router.beforeEach。
  3. 重用钩子:如果是在同一个组件内切换(仅 ID 变了),执行 beforeRouteUpdate。
  4. 路由配置钩子:执行配置表里的 beforeEnter。
  5. 解析钩子:执行 beforeResolve。

底层秘密:只要其中任何一个钩子返回 false 或者 reject,整个 router.push 产生的 Promise 就会直接中断,地址栏和视图都不会有任何变化。


第四阶段:改变表象(同步地址栏)

一旦所有守卫通过,Router 才会真正去动浏览器的东西。

  1. 物理修改:如果你是 History 模式,它执行 window.history.pushState(state, '', url)。如果你是 Hash 模式,它修改 location.hash。
  2. 注意:此时浏览器地址栏变了,但页面依然是旧的!

       原生限制:popstate 只有在用户点击后退、前进按钮,或者调用 history.back() 时才会触发。

       Vue Router 的解决方案:它重写了跳转方法。当你调用 router.push,它在内部同时执行了“改地址栏”和“触发 Vue 组件渲染”两个任务


第五阶段:核心跃迁(驱动视图重绘)

这是最后一步,也是最关键的一步:如何通知 Vue 更新界面?

  1. 修改响应式状态: Router 内部有一个核心状态(在 v4 中通常是 currentRoute)。此时,Router 会执行: currentRoute.value = toLocation;(这是一个 shallowRef)。
  2. 依赖触发: 你的组件树中一定包含 <router-view>。由于 <router-view> 在渲染时访问了 currentRoute,它就被作为“依赖”收集到了。
  3. 组件替换: currentRoute 的改变触发了 <router-view> 的 Re-render。它根据 toLocation.matched 里的组件定义,通过 Vue 的内建组件(如 <component :is="..."> 的底层逻辑)将旧组件卸载,新组件挂载。

路由懒加载:

正常加载:是在页面初始化时,通过 import Content from '...' 直接引入。这意味着无论用户是否访问该页面,组件代码都会被打包进 app.js(主包)中。

懒加载:使用 () => import('...') 这种动态导入语法。只有当用户 真正访问 那个路由时,浏览器才会去加载该组件对应的脚本

“路由懒加载本质上是一种**‘空间换时间’**的策略。

我们通过在磁盘上生成更多的分包文件(空间),换取了首屏加载时更少的网络传输量(时间)

assistant