3466 字
17 分钟
vue-router
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 模式)
-
构造流程:
- 保存 routes 配置
- 构建 mapRoute
- 将路由实例挂载到 Vue 原型(
Vue.prototype.router),便于全局访问 - 创建响应式的 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.$router、this.$route访问路由信息。 - 使用响应式对象,保证路径变化时视图自动刷新。
响应式更新机制详解: Vue.observable 创建的对象具有响应式特性,当对象的属性发生变化时,Vue 会自动追踪依赖并重新渲染相关组件。具体流程如下:
- 响应式对象创建:
Vue.observable({ path: '/home' })创建一个响应式对象 - 依赖收集:当组件在模板中使用
this.$route.path时,Vue 会建立组件与这个响应式对象的依赖关系 - 属性变化:当调用
Vue.prototype.route.path = newPath时,Vue 检测到响应式对象属性变化 - 依赖通知:Vue 通知所有依赖这个属性的组件进行重新渲染
- 视图更新:RouterView 组件重新执行 render 函数,根据新的 path 渲染对应的组件
这就是为什么修改 route.path 能够自动触发视图更新的核心原理。
3. 路由跳转与响应式更新
- push 方法:实现编程式导航,改变 URL 并触发视图更新。
push(path) { this.history.pushState({}, '', path); Vue.prototype.route.path = path;}设计说明:
- 通过
history.pushState修改地址栏,不刷新页面。 - 修改响应式 route 对象,自动驱动视图更新。
路由跳转完整流程:
- 用户触发:用户点击 RouterLink 或调用
this.$router.push() - URL 更新:
history.pushState()修改浏览器地址栏,不触发页面刷新 - 响应式更新:修改
Vue.prototype.route.path,触发 Vue 响应式系统 - 依赖通知:Vue 检测到响应式对象变化,通知相关组件重新渲染
- 组件重新渲染:RouterView 重新执行 render 函数,根据新路径渲染对应组件
- 视图更新:页面内容更新,用户看到新的页面内容
这个流程确保了路由跳转的完整性和用户体验的流畅性。
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,确定当前是第几级嵌套。
- 支持多级路由嵌套,灵活渲染。
嵌套路由渲染机制详解:
- 深度计算:通过
while循环向上遍历父组件,统计遇到的 router-view 数量,确定当前是第几级嵌套 - 路径匹配:根据当前路径,从 mapRoute 中筛选出所有匹配的路径(如
/home/haha会匹配到["/home", "/home/haha"]) - 层级对应:使用计算出的 depth 作为索引,从匹配的路径数组中取出对应层级的路径
- 组件渲染:根据路径从 mapRoute 中获取对应的组件,并通过
createElement渲染
这种设计巧妙解决了多级嵌套路由的渲染问题,每个 router-view 都能正确渲染对应层级的组件。
6. RouterLink 实现原理
- 渲染 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});响应式更新详细流程:
- 响应式对象创建:Vue.observable 将普通对象转换为响应式对象,内部使用 Object.defineProperty 或 Proxy 实现属性劫持
- 依赖收集阶段:当组件访问
this.$route.path时,Vue 的响应式系统会建立组件与这个属性的依赖关系 - 属性变化检测:当执行
Vue.prototype.route.path = newPath时,Vue 检测到属性变化 - 依赖通知机制:Vue 通知所有依赖这个属性的组件进行重新渲染
- 组件重新渲染:RouterView 等依赖组件重新执行 render 函数,根据新的 path 渲染对应组件
这种机制确保了路由变化时视图的自动更新,是 Vue 响应式系统的典型应用。
Q: 如何支持嵌套路由?
A: 通过递归注册和深度计算:
- 递归注册:
deepMapRoute函数递归处理 routes 配置,将所有层级的路由都注册到 mapRoute 中 - 深度计算:RouterView 组件通过向上遍历父组件,计算当前是第几级嵌套,从而渲染对应层级的组件
嵌套路由实现细节:
- 路由注册阶段:递归遍历 routes 配置,将所有路径(包括子路由)都注册到 mapRoute 对象中
- 深度识别机制:RouterView 通过
data.routerView = true标记自己,然后向上遍历父组件统计 router-view 数量 - 路径匹配算法:使用
path.includes(item)筛选出所有匹配的路径,并按层级排序 - 组件渲染策略:根据计算出的 depth 从匹配路径数组中取出对应层级的路径,渲染对应组件
这种设计支持任意层级的嵌套路由,每个 router-view 都能正确渲染对应层级的组件。
Q: 插件机制的作用是什么?
A: 插件机制实现了以下功能:
- 全局组件注册:将 RouterView 和 RouterLink 注册为全局组件
- 原型挂载:将路由实例挂载到 Vue 原型,所有组件都能访问
- 生命周期注入:通过 mixin 在组件创建时标记根实例
三、代码实现
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 MyRouterimport RouterView from './components/view'import RouterLink from './components/link'
//用于辨别当前内容是否为空,不为空返回truefunction 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 installimport 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); },};
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) }}