今天我们来研究一下 vue-router 的内部实现原理。
我们知道,vue app 是 spa,是没有页面之间的跳转的,说的直白一点就是没有刷新。那么在不刷新页面的情况下,怎么实现一个项目中,不同页面,不同组件之间的切换展示呢?
通过学习 vue-router 的源码,可以得知,浏览器为我们提供了两个方案:
- hash(就是页面上的 #)
- history(h5)
今天我们就简单来实现一下通过 hash 的方式实现 vue 的路由跳转。
Step 1 - 需求分析
首先通过 vue-cli 4.x 创建项目,选择使用 router,并且模式选择为 hash。
项目创建完成后,我们可以看到 src 目录下有一个 router 文件,其内容大致如下:
import Vue from 'vue'
import VueRouter from './yvue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue'),
},
]
const router = new VueRouter({
routes,
})
export default router
我们逐步来分析一下上面的代码:
- 首先是用过 Vue.use(VueRouter) 注册了插件,那么显然 VueRouter 是 Vue 的插件,需要有自己的静态 install 方法
- 接着是我们熟悉的,配置了路有映射表,用于确定路由和组件的关系
- 然后通过 new VueRouter({routers}) 创建了路由实例,并且将其导出
- 最后熟悉 vue 的同学肯定知道,导出的这个 vue,我们在 main 中将其引用并且传递给了 Vue 本身
上述流程非常简单,那么从 VueRouter 的角度出发,我们应该做些什么呢?
首先这里 Vue.use 的时候会调用 VueRouter.install 方法,而在这个方法里我们应该做些什么呢?
联合 vue-router 的使用我们知道,注册 router 之后,我们可以全局使用两个组件:
- router-view:用于显示组件
- router-link:用于路由跳转
显然,这两个组件的注册过程是在 install 中,因为这里拥有对全局 Vue 的引用。
接下来,就是 router 的核心功能,当 hash 改变的时候,改变展示的组件,显然这里我们应该有一个响应式的 current,当 current 改变的时候,就触发 render 重新渲染组件。
以上,我们可以大致总结一下,如下图所示:

Step 2 - 功能实现
那么现在我们可以着手开始实现自己的 vue-router 了。
首先我们肯定需要一个 VueRouter 的类,应为在后面它需要被实例化:
class VueRouter {
// 对应 Vue.use
static install(Vue) {}
// 对应 new
constructor(options) {}
}
紧接着我们就可以在 install 中实现对两个全局组件的注册:
class VueRouter {
// 对应 Vue.use
static install(Vue){
// step 1: 注册组件 router-link,用于路由跳转
// 回忆一下 router-link 的使用:
// * <router-link to="/login">login</router-link>
Vue.component('router-link', {
// 必要参数 to
props: {
to: {
type: String,
require: true,
},
},
// 模板,可以使用 template,也可以使用 render。
// * render 更加灵活,并且在 vue 中会把 template 编译成 render 函数。
render() {
// 这里可以写 jsx,也可以使用 h 函数
// 这里我们使用 jsx,在 view-router 中使用 h,都尝试一下
// * h 函数类似于 react 中的 createElement
return <a href={`#${this.to}`}>{this.$slots.default}</a>
}
// step 2: 注册组件 router-view,用于显示组件
// 回忆一下 router-link 的使用:
// <router-view />
Vue.component('router-view', {
// 没有参数,直接 template
render(h) {
// 这里使用 h
// view-router 的功能是渲染组件,那么组件从哪里获取呢?
// 回忆 router.js 的路由表,其中有映射 url 地址和组件,所以组件显然是从这里获取
// 而当前路由我们可以在构造函数中定义一个 current 来表示
// 所以可以通过以下代码找到当前 current 对应的组件
const { routeMap, current } = this.$router
const component = routeMap[current] ? routeMap[current].component : null
return h(component)
},
})
}
/*...*/
}
到这里,至少我们的 已经是可用的了,而 则需要我们在构造函数中实现对路由表的管理以及对 hashchange 的监听。
那么接下来自然是实现 constructor
class VueRouter {
/*...*/
// 对应 new
constructor(options) {
// 初始化路由表
this.routerMap = {}
options.routes.map((route) => {
this.routerMap[route.path] = route
})
// 定义 current
const current = window.location.hash.slice(1) || '/'
// 监听 hashchange
window.addEventListener('hashchange', this.onHashChange.bind(this))
}
onHashChange() {
this.current = window.location.hash.slice(1)
}
}
到这里,整个 vue-router 已经实现得差不多了,但还存在这一个巨大的问题,那就是 current 并不是响应式的,当路由切换,current 改变的时候,并不会触发 render 重新渲染组件:

那应该怎么办呢?
接下来的问题是:怎么将 current 转成 vue 响应式数据?
对于上述问题,这里有两个思路:
- 新建 vue 实例,通过 data 将 current 转成响应式数据
- 利用 vue 提供的静态方法 Vue.util.defineReactive
这里我们采取第二种方案:
class VueRouter {
/*...*/
// 对应 new
constructor(options) {
// 初始化路由表
this.routerMap = {}
options.routes.map((route) => {
this.routerMap[route.path] = route
})
// 定义 current
const initial = window.location.hash.slice(1) || '/'
Vue.util.defineReactive(this, 'current', initial)
// 监听 hashchange
window.addEventListener('hashchange', this.onHashChange.bind(this))
}
onHashChange() {
this.current = window.location.hash.slice(1)
}
}
到这里又出现一个问题,这个 Vue 从哪里来呢?
其实这里需要让思维跳出 constructor:我们知道执行 install 的时候会传入 Vue,那么在 install 的时候我们保存一个全局变量 _Vue 即可在 constructor 中进行使用了:
let _Vue
class VueRouter {
/*...*/
// 对应 new
constructor(options) {
// 初始化路由表
this.routerMap = {}
options.routes.map((route) => {
this.routerMap[route.path] = route
})
// 定义 current
const initial = window.location.hash.slice(1) || '/'
_Vue.util.defineReactive(this, 'current', initial)
// 监听 hashchange
window.addEventListener('hashchange', this.onHashChange.bind(this))
}
onHashChange() {
this.current = window.location.hash.slice(1)
}
}
到这里,基本的路由已经实现了:

还有一些可以扩展的地方:
比如优化一下 constructor,将 options 保存到本地:
constructor(options){
this.$options = options
// 初始化路由表
this.routerMap = {}
this.$options.routes.map((route)=>{
this.routerMap[route.path] = route
})
// 定义 current
const initial = window.location.hash.slice(1) || '/'
_Vue.util.defineReactive(this, 'current', initial)
// 监听 hashchange
window.addEventListener('hashchange', this.onHashChange.bind(this))
}
这可以保证数据的单向流动。
比如为组件实例挂载 router.push() 这样使用 router 上的方法了。
不过这里有一个点值得注意,那就是时间:
因为当我们执行 install 的时候,VueRouter 的实例还没创建,那么直接通过:
Vue.prototype.$router = this.$options.router
进行挂载显然是不行的。
这里我们可以借助 mixin 来使用 Vue 的生命周期钩子,当组件创建之前进行挂载操作:
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
},
})
这样就没什么问题了。
结语
更佳阅读体验:手写简易 vue-router