很多大佬都说过,写代码大概有这么几个阶段:知道怎么用 => 知道怎么实现 => 知道为什么这样实现 => 知道怎么实现更好 。 源码分析,则是进阶的一个比较好的方式。今天分享下对 vue-router 的理解,输出倒逼输入吧。
使用方法
先回顾一下vue-router的使用方法,
首先编写vue-router配置文件,
// router/index.js
import Vue from 'vue'
import VueRouter from '../vue-router'
import Home from '../views/home';
import About from '../views/about';
let routes = [
{
path: '/',
component: Home
},
{
path: '/about',
component: About,
children: [
{
path: 'a',
component: {
render: (h) => h('h3', 'about a')
}
},
{
path: 'b',
component: {
render: (h) => h('h3', 'about b')
}
}
]
},
]
Vue.use(VueRouter)
export default new VueRouter({
mode: 'hash',
routes,
})
在main.js里引入,
//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router/index'
Vue.config.productionTip = false
new Vue({
name: 'root',
render: h => h(App),
router,
}).$mount('#app')
在App.vue里使用router-view、router-link组件;
<template>
<div id="app">
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
源码分析
OK,从配置文件开始分析:
Vue.use(VueRouter)
install
利用vue的插件机制,使用Vue.use(VueRouter)时,会调用vue-router的install方法,装载vue-router,install方法如下:
export let Vue
import RouterLink from './components/router-link'
import RouterView from './components/router-view'
const install = function(_Vue) {
Vue = _Vue
Vue.component('router-link', RouterLink)
Vue.component('router-view', RouterView)
Vue.mixin({
beforeCreate() {
if (this.$options.router) {
this._routerRoot = this; // 将当前根实例 放到_routerRoot
this._router = this.$options.router;
this._router.init(this); // 调用实例的init方法 初始化vue-router
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else { // 子组件获取父组件的_routerRoot
this._routerRoot = this.$parent && this.$parent._routerRoot;
}
},
})
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this._routerRoot._route
}
})
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this._routerRoot._router
}
})
}
export default install
在install内部定义二个全局组件router-link、router-view;
使用 Vue 混入机制,在 Vue 的生命周期 beforeCreate 钩子函数中混入,用户将router属性注册到new Vue, 在这里,按照先父后子的顺序,让所有组件都可以获取到router属性,然后调用实例的init方法 初始化vue-router;
用 defineReactive 方法定义 _route 属性为响应式,后面再讲它的作用;
为了方便使用,使用Object.defineProperty定义了 $route、$router二个属性,$route 存放的都是属性,$router 是router实例;
VueRouter对象
接下来,看下 VueRouter 做了哪些事:
import createMatcher from './create-matcher'
import HashHistory from './history/hashHistory'
import BrowserHistory from './history/browserHistory'
class VueRouter {
constructor(options) {
let routes = options.routes || []
this.matcher = createMatcher(routes)
this.mode = options.mode || 'hash'
switch(this.mode) {
case 'hash':
this.history = new HashHistory(this);
break;
case 'history':
this.history = new BrowserHistory(this);
break;
}
}
init(app) {
const history = this.history;
let setupHashListener = () => {
history.setupListener()
}
history.transitionTo(history.getCurrentLocation(), setupHashListener)
history.listen((route) => {
app._route = route
})
}
push(location) {
// 执行this.$router.push(), hash变化,执行transitionTo
window.location.hash = location
}
match(location) {
return this.matcher.match(location)
}
}
export default VueRouter
1)调用createMatcher,创建匹配器, 它的作用是添加路由匹配,以及动态路由添加;
2)然后根据用户传入的mode,创建不同的历史管理实例history;
3)重要的是init方法,执行history的transitionTo方法,对跳转路径进行匹配,跳转完毕后,还需要监听路由的变化,执行setupHashListener,就是执行了window.addEventListener('hashchange', ()=> {this.transitionTo();}) (参看后面的 setupListener 方法实现);
4)初始化时还需要调用更新 _route 的方法;
createMatcher
先说第一点 createMatcher,因为传入的routes 并不方便使用,转换成map更方便,例如:{path: 'a', children: [{path: 'b'}]}, 被 createMatcher 转换成 {'a': {path: 'a', component:A}, 'a/b': {path: 'a/b',compoent:B, parent: 'a'}},更加方便匹配;
match方法通过用户输入的路径,来获取对应的匹配记录,然后通过 createRoute 创建新路由记录;
addRoutes方法则将用户添加的路由,与存在的进行合并;
export default function createMatcher(routes) {
let {pathList, pathMap} = createRouteMap(routes)
function match(location) {
let record = pathMap[location];
return createRoute(record, {
path: location
})
}
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap)
}
return {
match,
addRoutes
}
}
History
再说第二点,先看下父类History,
export default class History {
constructor(router) {
this.router = router;
this.current = createRoute(null, {
path: '/'
})
}
transitionTo(location, complete) {
let current = this.router.match(location)
// 判断是否重复路径
if(location == this.current.path && this.current.matched.length === current.matched.length) {
return
}
this.current = current
// 希望current变化 , 更新_route, 视图就可以更新
this.cb && this.cb(current)
complete && complete()
}
listen(cb) {
this.cb = cb
}
}
子类HashHistory:
const ensureSlash = () => {
if(window.location.hash) {
return
}else {
window.location.hash = '/'
}
}
function getHash(){
return window.location.hash.slice(1);
}
export default class HashHistory extends History {
constructor(router) {
super(router)
this.router = router;
// 使用hash模式,默认如果没有#,应该跳转到 #/
ensureSlash()
}
getCurrentLocation() {
return getHash()
}
setupListener() {
window.addEventListener('hashchange', ()=> {
this.transitionTo(getHash());
})
}
}
子类的 setupListener 会在 init 中被调用,用来监听路由的变化;
在History类里定义了current属性,对应路径匹配的记录,transitionTo方法根据当前location,获取对应的路径匹配记录,然后在router-view中渲染页面。
渲染页面
渲染这一步具体是如何做的呢?
结合2、3、4点一起分析, 我们把根 Vue 实例的_route属性定义成响应式的,
Vue.util.defineReactive(this, '_route', this._router.history.current)
然后在每个 router-view 组件执行 render 函数的时候,都会访问 parent.$route,触发了它的 getter,
export default {
name: 'router-view',
functional: true,
render(h, {parent, data}) {
// 访问parent.$route
let route = parent.$route
let depth = 0
data.routerView = true // 标识路由属性
while(parent) {
if(parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
parent = parent.$parent
}
let record = route.matched[depth]
if(!record) {
return h()
}
return h(record.component, data)
}
}
然后再执行完 transitionTo 后,修改 current 的时候, app._route 也被修改,于是触发了setter,因此会通知 router-view 的渲染 watcher 更新,重新渲染组件。
以上,分析了 vue-router 的核心代码的关键思路,有问题敬请指证,一起探讨。