3466 字
17 分钟
vue-router
2026-02-19
2026-02-19
统计加载中...

vue-router#

一、核心实现思路#

1. 路由映射表的构建#

  • 目的:将用户配置的 routes 数组,转换为路径与组件的映射关系,便于后续查找和渲染。
  • 实现:通过递归函数 deepMapRoute,遍历所有路由及其子路由,将每个 path 映射到对应的 component。
function deepMapRoute(routes) {
routes.forEach((item) => {
this[item.path] = item.component;
if (item.children instanceof Array && item.children.length) {
deepMapRoute.call(this, item.children);
}
});
}

设计说明:递归处理嵌套路由,保证多级路由都能被正确注册。

2. MyRouter 类的设计#

  • 构造参数:接收 routes 配置。

  • 核心属性

    • routes:原始路由配置
    • mapRoute:路径到组件的映射表
    • history:浏览器 history 对象(仅支持 history 模式)
  • 构造流程

    1. 保存 routes 配置
    2. 构建 mapRoute
    3. 将路由实例挂载到 Vue 原型(Vue.prototype.router),便于全局访问
    4. 创建响应式的 route 对象(Vue.prototype.route),用于追踪当前路径,实现视图自动更新
class MyRouter {
constructor(options) {
this.routes = options.routes;
this.mapRoute = {};
this.history = window.history;
deepMapRoute.call(this.mapRoute, this.routes);
Vue.prototype.router = this;
Vue.prototype.route = Vue.observable({
path: new URL(window.location).pathname
});
}
// ...
}

设计说明

  • 挂载到 Vue 原型,方便所有组件通过 this.$routerthis.$route 访问路由信息。
  • 使用响应式对象,保证路径变化时视图自动刷新。

响应式更新机制详解: Vue.observable 创建的对象具有响应式特性,当对象的属性发生变化时,Vue 会自动追踪依赖并重新渲染相关组件。具体流程如下:

  1. 响应式对象创建Vue.observable({ path: '/home' }) 创建一个响应式对象
  2. 依赖收集:当组件在模板中使用 this.$route.path 时,Vue 会建立组件与这个响应式对象的依赖关系
  3. 属性变化:当调用 Vue.prototype.route.path = newPath 时,Vue 检测到响应式对象属性变化
  4. 依赖通知:Vue 通知所有依赖这个属性的组件进行重新渲染
  5. 视图更新:RouterView 组件重新执行 render 函数,根据新的 path 渲染对应的组件

这就是为什么修改 route.path 能够自动触发视图更新的核心原理。

3. 路由跳转与响应式更新#

  • push 方法:实现编程式导航,改变 URL 并触发视图更新。
push(path) {
this.history.pushState({}, '', path);
Vue.prototype.route.path = path;
}

设计说明

  • 通过 history.pushState 修改地址栏,不刷新页面。
  • 修改响应式 route 对象,自动驱动视图更新。

路由跳转完整流程

  1. 用户触发:用户点击 RouterLink 或调用 this.$router.push()
  2. URL 更新history.pushState() 修改浏览器地址栏,不触发页面刷新
  3. 响应式更新:修改 Vue.prototype.route.path,触发 Vue 响应式系统
  4. 依赖通知:Vue 检测到响应式对象变化,通知相关组件重新渲染
  5. 组件重新渲染:RouterView 重新执行 render 函数,根据新路径渲染对应组件
  6. 视图更新:页面内容更新,用户看到新的页面内容

这个流程确保了路由跳转的完整性和用户体验的流畅性。

4. 插件注册与全局组件#

  • install 方法:实现 Vue 插件规范,注册全局组件和混入。

  • 全局组件

    • RouterView:根据当前路径渲染对应组件,支持多级嵌套路由。
    • RouterLink:实现声明式导航,生成 a 标签并绑定点击事件,调用 push 方法。

install.js 关键代码:#

function install(Vue) {
Vue.component('RouterView', RouterView);
Vue.component('RouterLink', RouterLink);
Vue.mixin({
beforeCreate() {
if (this.$options.router !== undefined) {
this._routerRoot = this;
}
}
});
}

设计说明

  • 注册全局组件,方便模板中直接使用 <router-view><router-link>
  • 通过 mixin 标记根 Vue 实例,便于多级 router-view 计算。

5. RouterView 实现原理#

  • 通过 depth 变量,支持多级嵌套路由。
  • 根据当前路径和深度,查找 mapRoute 中对应的组件并渲染。
render: (_, { parent, data }) => {
data.routerView = true;
let depth = 0;
while (parent && parent._routerRoot !== false) {
let vnodeData = parent.$vnode ? parent.$vnode.data : {};
if (vnodeData.routerView) depth++;
parent = parent.$parent;
}
let path = Vue.prototype.route.path;
let pathMap = Object.keys(Vue.prototype.router.mapRoute).filter(item => path.includes(item));
let currentPath = pathMap[depth];
if (!currentPath) return;
let component = Vue.prototype.router.mapRoute[currentPath];
return parent.$createElement(component, data);
}

设计说明

  • 递归向上查找 router-view,确定当前是第几级嵌套。
  • 支持多级路由嵌套,灵活渲染。

嵌套路由渲染机制详解

  1. 深度计算:通过 while 循环向上遍历父组件,统计遇到的 router-view 数量,确定当前是第几级嵌套
  2. 路径匹配:根据当前路径,从 mapRoute 中筛选出所有匹配的路径(如 /home/haha 会匹配到 ["/home", "/home/haha"]
  3. 层级对应:使用计算出的 depth 作为索引,从匹配的路径数组中取出对应层级的路径
  4. 组件渲染:根据路径从 mapRoute 中获取对应的组件,并通过 createElement 渲染

这种设计巧妙解决了多级嵌套路由的渲染问题,每个 router-view 都能正确渲染对应层级的组件。

  • 渲染 a 标签,绑定点击事件,阻止默认跳转,调用 push 方法实现路由切换。
render: (_, { parent, props, data, children }) => {
let createElement = parent.$createElement;
data.on = {
click: function (event) {
event.preventDefault();
Vue.prototype.router.push(props.path);
}
};
data.attrs.href = props.path;
return createElement(props.tag, data, children);
}

二、面试话术与理解要点#

1. 核心概念理解#

Q: 请简述前端路由的实现原理?

A: 前端路由的核心原理是通过监听 URL 变化,在不刷新页面的情况下动态切换页面内容。主要包含三个核心部分:

  • 路由注册:将路径与组件建立映射关系
  • 路由监听:监听 URL 变化(通过 history API 或 hash 变化)
  • 视图更新:根据当前路径渲染对应组件

Q: 为什么需要前端路由?

A: 前端路由解决了传统多页面应用的痛点:

  • 用户体验:避免页面刷新,提供更流畅的交互体验
  • 性能优化:只更新变化的部分,减少不必要的资源加载
  • 状态保持:在页面切换过程中保持应用状态
  • SEO 友好:每个路由都有独立的 URL,便于搜索引擎收录

2. 技术实现要点#

Q: vue-router 是如何实现响应式更新的?

A: 通过 Vue.observable 创建响应式对象:

Vue.prototype.route = Vue.observable({
path: new URL(window.location).pathname
});

响应式更新详细流程

  1. 响应式对象创建:Vue.observable 将普通对象转换为响应式对象,内部使用 Object.defineProperty 或 Proxy 实现属性劫持
  2. 依赖收集阶段:当组件访问 this.$route.path 时,Vue 的响应式系统会建立组件与这个属性的依赖关系
  3. 属性变化检测:当执行 Vue.prototype.route.path = newPath 时,Vue 检测到属性变化
  4. 依赖通知机制:Vue 通知所有依赖这个属性的组件进行重新渲染
  5. 组件重新渲染:RouterView 等依赖组件重新执行 render 函数,根据新的 path 渲染对应组件

这种机制确保了路由变化时视图的自动更新,是 Vue 响应式系统的典型应用。

Q: 如何支持嵌套路由?

A: 通过递归注册和深度计算:

  • 递归注册deepMapRoute 函数递归处理 routes 配置,将所有层级的路由都注册到 mapRoute 中
  • 深度计算:RouterView 组件通过向上遍历父组件,计算当前是第几级嵌套,从而渲染对应层级的组件

嵌套路由实现细节

  1. 路由注册阶段:递归遍历 routes 配置,将所有路径(包括子路由)都注册到 mapRoute 对象中
  2. 深度识别机制:RouterView 通过 data.routerView = true 标记自己,然后向上遍历父组件统计 router-view 数量
  3. 路径匹配算法:使用 path.includes(item) 筛选出所有匹配的路径,并按层级排序
  4. 组件渲染策略:根据计算出的 depth 从匹配路径数组中取出对应层级的路径,渲染对应组件

这种设计支持任意层级的嵌套路由,每个 router-view 都能正确渲染对应层级的组件。

Q: 插件机制的作用是什么?

A: 插件机制实现了以下功能:

  • 全局组件注册:将 RouterView 和 RouterLink 注册为全局组件
  • 原型挂载:将路由实例挂载到 Vue 原型,所有组件都能访问
  • 生命周期注入:通过 mixin 在组件创建时标记根实例

三、代码实现#

index.js
import Vue from 'vue';
import install from './install';
function deepMapRoute(routes){
routes.forEach((item)=>{
this[item.path]=item.component;
//此处检查当前路由下,是否还有子路由children,如果有就递归遍历子路由
if(item.children instanceof Array&&item.children.length){
deepMapRoute.call(this,item.children);
}
})
}
class MyRouter{
constructor(options){
//当new MyRouter创建实例对象时,会调用该函数
//获取配置对象中的routes数组
this.routes = options.routes;
//创建mapRoute对象,用于存储路径与组件之间的映射关系(方便后续查找)
this.mapRoute = {};
/*
此处仅实现mode:history模式,并未实现hashHistory模式
history模式的实现原理是通过H5新增的API--window.history实现对浏览器历史记录栈的操作
*/
this.history = window.history;
/*
通过递归对用户传入的routes所有路由进行结构转换
用户传入:
routes:[
{
path:"/home",
component:Home,
children:[
{
path:"/home/xixi",
component:Xixi
}
]
},
{
path:"/about",
component:About
}
]
转换之后的mapRoutes:
{
"/home":{
component:Home,
children:{
"/home/xixi":{
component:Xixi
}
}
},
"/about":{
component:About
}
}
*/
deepMapRoute.call(this.mapRoute,this.routes);
/*
将当前的路由器实例对象,放到Vue原型对象上,所有的Vue组件都能看得到
例如:Vue组件内部使用this.$router
*/
Vue.prototype.router = this;
/*
Vue.observable()可以将某个普通对象,变成响应式对象,响应式对象的属性值发生修改,Vue视图会重新渲染
将当前的响应式对象,放到Vue原型对象上,所有的Vue组件都能看得到(可以理解为所有组件共享的data)
new URL(window.location)可以得到URL对象,从pathname属性中,可以获得当前的路由地址
此处是为了辨别用户一上来的路由路径是什么,例如:http://localhost:8080/about ---> 得到的结果就是/about
例如:Vue组件内部使用this.$route
*/
Vue.prototype.route = Vue.observable({
path:new URL(window.location).pathname
})
}
/*
该方法用于向浏览器历史记录栈中推送记录,控制URL地址变化,并控制Vue组件重新渲染
*/
push(path){
//控制URL地址变化
this.history.pushState({},"",path);
/*
修改Vue原型对象内的route属性的path值的变化
在constructor,我们生成了一个响应式对象route,内部的path属性发生变化,会导致Vue组件重新渲染
*/
Vue.prototype.route.path=path;
}
}
//想使用Vue.use(MyRouter)语法,必须提供install方法
MyRouter.install=install
export default MyRouter
install.js
import RouterView from './components/view'
import RouterLink from './components/link'
//用于辨别当前内容是否为空,不为空返回true
function isDep(v){
return v!==undefined;
}
/*
想要使用Vue.use()语法声明使用当前插件,需要提供一个install方法
例如:Vue.use(VueRouter)
*/
function install(Vue){
/*
注册全局组件router-link和router-view
router-link:默认生成a标签,实现声明式导航
router-view:用于显示对应层级的路由组件
*/
Vue.component("RouterView",RouterView);
Vue.component("RouterLink",RouterLink);
/*
Vue.mixin()用来向所有的Vue组件注入生命周期钩子函数
下面的代码是可以让所有组件在beforeCreate阶段都执行内部的代码
*/
Vue.mixin({
beforeCreate(){
if(isDep(this.$options.router)){
/*
如果能进入该判断,说明当前对象的$options对象中具有router属性
例如:new Vue({
router
})
给当前的Vue组件添加一个_routerRoot属性,并指向自己
作用:声明当前实例对象是路由的根组件
注意:整个Vue项目中,只有main.js中new Vue()得到的实例对象,有资格拥有_routerRoot属性
*/
this._routerRoot=this;
}
}
})
}
export default install
compoents/links.js
import Vue from 'vue';
/*
该文件是router-link的源码实现
router-link是函数组件
实现原理:
1.默认生成a标签
2.给当前a标签绑定点击事件
3.禁止a标签的默认事件,防止他自动跳转
4.在点击事件内部,调用编程式导航this.$router.push方法
*/
export default {
name: 'RouterLink',
functional: true,
props: {
tag: {
type: String,
default: 'a',
},
path: {
type: String.require,
},
},
render: (_, { parent, props, data, children }) => {
/*
获取父组件创建虚拟DOM的方法
*/
let createElement = parent.$createElement;
/*
绑定点击事件,并禁止a标签的默认行为
使用编程式导航push方法,根据props传递下来的path属性值,实现URL地址变化
*/
data.on = {
click: function (event) {
event.preventDefault();
Vue.prototype.router.push(props.path);
},
};
/*
将props传递下来的path属性值赋值给a标签,作为a标签的href属性
*/
data.attrs.href = props.path;
/*
通过createElement方法生成虚拟DOM
children是组件标签之间写的内容,例如<router-link>aaa</router-link>,那children就是"aaa"
*/
return createElement(props.tag, data, children);
},
};
components/view.js
import Vue from 'vue';
/*
该文件是router-view的源码实现
router-view是函数组件
实现原理:
1.声明当前组件是router-view组件
2.通过depth变量,记录当前是几级路由
(通过while循环,从当前组件往上找,看遇到了几个rouiter-view,直至找到路由根组件为止)
3.通过router上的mapRoutes对象,配合当前的路由地址,搜索出所有路径相似的路由,获取到对应的组件
4.将对应的组件通过createElement方法,生成虚拟DOM
*/
export default {
name:"RouterView",
functional:true,
render:(_,{parent,props,data,children})=>{
// 声明当前组件是router-view组件
data.routerView = true;
// 通过depth变量,记录当前是几级路由
let depth=0;
// 获取父组件创建虚拟DOM的方法
let createElement = parent.$createElement;
while(parent&&parent._routerRoot!=false){
let vnodeData = parent.$vnode?parent.$vnode.data:{};
if(vnodeData.routerView){
// 在向上找的过程中,遇到一个router-view组件,就将depth+1,用来记录当前的router-view用来显示第几级路由
depth++;
}
parent=parent.$parent;
}
//获取当前的路由地址
let path = Vue.prototype.route.path;
/*
1.先提取出当前的路由注册表对象所有的key(也就是所有注册的路径),得到有路由路径组成的数组
2.再从数组中过滤出,与当前路由地址相关的路径组成的数组
(例如当前路由地址:/home/haha => 得到的数组["/home","/home/haha"])
*/
let pathMap = Object.keys(Vue.prototype.router.mapRoute).filter((item)=>{
return path.includes(item);
});
let currentPath = pathMap[depth];
if(!currentPath)return;
let component = Vue.prototype.router.mapRoute[currentPath];
return createElement(component,data)
}
}