传统跳转(全量刷新)
当你访问 a.com/page1 跳到 a.com/page2 时:
- 浏览器销毁当前页面的所有变量(Vue 实例、未提交的 Vuex/Pinia 数据、定时器)。
- 向服务器请求新的 HTML。
- 重新加载 CSS、重新下载并执行 JS(即便两个页面的 JS 是一模一样的)。
- 结果: 性能浪费,且无法维持跨页面的动画或复杂的应用状态。
Vue Router(组件映射)
Vue Router 的出现,就是为了在 “不刷新页面” 的前提下,模拟出 “换页” 的效果。
它建立了一张映射表:
| URL 路径 | 对应的 Vue 组件 |
| :--- | :--- |
| /home | Home.vue |
| /user/:id | User.vue |
它的工作流程是:
- 感知变化:通过监听 popstate 或 hashchange 知道 URL 变了。
- 查找映射:在内存里的路由表查到:“哦,现在该看 User 组件了”。
- 局部替换:Vue Router 通知 Vue 引擎:“把页面里那个 <router-view> 位置的组件切成 User 吧”
实现了三个维度的“桥接”:
- URL 与 组件的映射:让每一个 URL 都有一个对应的 UI 界面。
- History 与 内存状态的同步:在不刷新页面的前提下,通过劫持浏览器历史记录,让“后退/前进”按钮在单页应用中依然有效。
- 导航生命周期的管理:提供了一套拦截机制(钩子函数),让开发者能在页面切换的“缝隙”里处理权限、埋点或异步数据加载
第一层: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 记录管理器。
- 参数规格化:Router 首先判断你传的是字符串还是对象。如果是字符串(如上述示例),它会立刻将其解析为一个 Location 对象。
- 预判拦截:它会检查新路径是否与当前路径完全一致。如果完全一样(且没有强制刷新的逻辑),它可能会直接拦截,避免重复跳转产生的冗余性能开销。
第二阶段:寻图定位(Matcher 匹配器)
这是最硬核的一步。Router 需要弄清楚:这个字符串路径到底对应谁?
- 正则提取:Matcher 拿着 /algorithm/detail/123 去路由表里匹配。
- 得分计算 (Ranking):它发现你的路由配置里有一个 { path: '/algorithm/detail/:id', name: 'AlgDetail', component: Detail }。Matcher 内部的 pathToRegexp 跑了一遍,确认匹配成功,并从 URL 中提取出 params: { id: '123' }。
- 生成匹配记录 (Matched Records):如果是嵌套路由,Matcher 会生成一个数组(如 [ParentRecord, ChildRecord])。输出:一个 Route 对象(即我们平时用的 $route 的雏形),里面包含了完整的 path, params, query 和 meta。
第三阶段:过五关斩六将(导航守卫流水线)
在 URL 真正改变之前,必须通过一系列的“安检”。Vue Router 4 将这些钩子函数串联成了一个 Promise 链。
- 失活钩子:调用当前页面组件的 beforeRouteLeave。
- 全局前置:执行 router.beforeEach。
- 重用钩子:如果是在同一个组件内切换(仅 ID 变了),执行 beforeRouteUpdate。
- 路由配置钩子:执行配置表里的 beforeEnter。
- 解析钩子:执行 beforeResolve。
底层秘密:只要其中任何一个钩子返回 false 或者 reject,整个 router.push 产生的 Promise 就会直接中断,地址栏和视图都不会有任何变化。
第四阶段:改变表象(同步地址栏)
一旦所有守卫通过,Router 才会真正去动浏览器的东西。
- 物理修改:如果你是 History 模式,它执行 window.history.pushState(state, '', url)。如果你是 Hash 模式,它修改 location.hash。
- 注意:此时浏览器地址栏变了,但页面依然是旧的!
原生限制:popstate 只有在用户点击后退、前进按钮,或者调用 history.back() 时才会触发。
Vue Router 的解决方案:它重写了跳转方法。当你调用 router.push,它在内部同时执行了“改地址栏”和“触发 Vue 组件渲染”两个任务
第五阶段:核心跃迁(驱动视图重绘)
这是最后一步,也是最关键的一步:如何通知 Vue 更新界面?
- 修改响应式状态: Router 内部有一个核心状态(在 v4 中通常是 currentRoute)。此时,Router 会执行: currentRoute.value = toLocation;(这是一个 shallowRef)。
- 依赖触发: 你的组件树中一定包含 <router-view>。由于 <router-view> 在渲染时访问了 currentRoute,它就被作为“依赖”收集到了。
- 组件替换: currentRoute 的改变触发了 <router-view> 的 Re-render。它根据 toLocation.matched 里的组件定义,通过 Vue 的内建组件(如 <component :is="..."> 的底层逻辑)将旧组件卸载,新组件挂载。
路由懒加载:
正常加载:是在页面初始化时,通过 import Content from '...' 直接引入。这意味着无论用户是否访问该页面,组件代码都会被打包进 app.js(主包)中。
懒加载:使用 () => import('...') 这种动态导入语法。只有当用户 真正访问 那个路由时,浏览器才会去加载该组件对应的脚本
“路由懒加载本质上是一种**‘空间换时间’**的策略。
我们通过在磁盘上生成更多的分包文件(空间),换取了首屏加载时更少的网络传输量(时间)