Vue 源码深入解析之 Vue Router、路由注册 和 VueRouter 对象
一、Vue-Router
- 路由,它的作用就是根据不同的路径映射到不同的视图。我们在用
Vue
开发过实际项目的时候都会用到Vue-Router
这个官方插件来帮我们解决路由的问题。Vue-Router
的能力十分强大,它支持hash
、history
、abstract
三种路由方式,提供了<router-link>
和<router-view>
两种组件,还提供了简单的路由配置和一系列好用的API
。 大部分人已经掌握了路由的基本使用,但使用的过程中也难免会遇到一些坑,那么这里我们就来深挖
Vue-Router
的实现细节,一旦我们掌握了它的实现原理,那么就能在开发中对路由的使用更加游刃有余。同样我们也会通过一些具体的示例来配合讲解,先来看一个最基本使用例子:
Hello App!
Go to Foo
Go to Bar
import Vue from ‘vue’
import VueRouter from ‘vue-router’
import App from ‘./App’Vue.use(VueRouter)
// 1. 定义(路由)组件。
// 可以从其他文件 import 进来
const Foo = { template: ‘foo‘ }
const Bar = { template: ‘bar‘ }// 2. 定义路由
// 每个路由应该映射一个组件。 其中”component” 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: ‘/foo’, component: Foo },
{ path: ‘/bar’, component: Bar }
]// 3. 创建 router 实例,然后传
routes
配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写)相当于 routes: routes
})// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
el: ‘#app’,
render(h) {return h(App)
},
router
})
这是一个非常简单的例子,接下来我们先从
Vue.use(VueRouter)
说起。
二、路由注册
Vue
从它的设计上就是一个渐进式JavaScript
框架,它本身的核心是解决视图渲染的问题,其它的能力就通过插件的方式来解决。Vue-Router
就是官方维护的路由插件,在介绍它的注册实现之前,我们先来分析一下Vue
通用的插件注册原理。Vue.use
,Vue
提供了Vue.use
的全局API
来注册这些插件,所以我们先来分析一下它的实现原理,定义在vue/src/core/global-api/use.js
中:export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}Vue.use
接受一个plugin
参数,并且维护了一个_installedPlugins
数组,它存储所有注册过的plugin
;接着又会判断plugin
有没有定义install
方法,如果有的话则调用该方法,并且该方法执行的第一个参数是Vue
;最后把plugin
存储到installedPlugins
中。- 由此可以看到
Vue
提供的插件注册机制很简单,每个插件都需要实现一个静态的install
方法,当我们执行Vue.use
注册插件的时候,就会执行这个install
方法,并且在这个install
方法的第一个参数我们可以拿到Vue
对象,这样的好处就是作为插件的编写方不需要再额外去import Vue
了。 路由安装,
Vue-Router
的入口文件是src/index.js
,其中定义了VueRouter
类,也实现了install
的静态方法:VueRouter.install = install
,它的定义在src/install.js
中,如下所示:export let _Vue
export function install (Vue) {
if (install.installed && _Vue === Vue) return
install.installed = true_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
Object.defineProperty(Vue.prototype, ‘$router’, {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, ‘$route’, {
get () { return this._routerRoot._route }
})
Vue.component(‘RouterView’, View)
Vue.component(‘RouterLink’, Link)const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}用户执行
Vue.use(VueRouter)
的时候,实际上就是在执行install
函数,为了确保install
逻辑只执行一次,用了install.installed
变量做已安装的标志位。另外用一个全局的_Vue
来接收参数Vue
,因为作为Vue
的插件对Vue
对象是有依赖的,但又不能去单独去import Vue
,因为那样会增加包体积,所以就通过这种方式拿到Vue
对象。Vue-Router
安装最重要的一步就是利用Vue.mixin
去把beforeCreate
和destroyed
钩子函数注入到每一个组件中。Vue.mixin
的定义,在vue/src/core/global-api/mixin.js
中:export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {this.options = mergeOptions(this.options, mixin)
return this
}
}它的实现实际上非常简单,就是把要混入的对象通过
mergeOptions
合并到Vue
的options
中,由于每个组件的构造函数都会在extend
阶段合并Vue.options
到自身的options
中,所以也就相当于每个组件都定义了mixin
定义的选项。- 回到
Vue-Router
的install
方法,先看混入的beforeCreate
钩子函数,对于根Vue
实例而言,执行该钩子函数时定义了this._routerRoot
表示它自身;this._router
表示VueRouter
的实例router
,它是在new Vue
的时候传入的;另外执行了this._router.init()
方法初始化router
,这个逻辑之后介绍,然后用defineReactive
方法把this._route
变成响应式对象,这个作用我们之后会介绍。而对于子组件而言,由于组件是树状结构,在遍历组件树的过程中,它们在执行该钩子函数的时候this._routerRoot
始终指向的离它最近的传入了router
对象作为配置而实例化的父实例。 - 对于
beforeCreate
和destroyed
钩子函数,它们都会执行registerInstance
方法。接着给Vue
原型上定义了$router
和$route
两个属性的get
方法,这就是为什么我们可以在组件实例上可以访问this.$router
以及this.$route
。接着又通过Vue.component
方法定义了全局的<router-link>
和<router-view>
两个组件,这也是为什么我们在写模板的时候可以使用这两个标签。最后定义了路由中的钩子函数的合并策略,和普通的钩子函数一样。 - 总结:我们分析了
Vue-Router
的安装过程,Vue
编写插件的时候通常要提供静态的install
方法,我们通过Vue.use(plugin)
时候,就是在执行install
方法。Vue-Router
的install
方法会给每一个组件注入beforeCreate
和destoryed
钩子函数,在beforeCreate
做一些私有属性定义和路由初始化工作,接下来我们就来分析一下VueRouter
对象的实现和它的初始化工作。
三、VueRouter 对象
VueRouter
的实现是一个类,我们先对它做一个简单地分析,它的定义在src/index.js
中:export default class VueRouter {
static install: () => void;
static version: string;app: any;
apps: Array;
ready: boolean;
readyCbs: Array;
options: RouterOptions;
mode: string;
history: HashHistory | HTML5History | AbstractHistory;
matcher: Matcher;
fallback: boolean;
beforeHooks: Array<?NavigationGuard>;
resolveHooks: Array<?NavigationGuard>;
afterHooks: Array<?AfterNavigationHook>;constructor (options: RouterOptions = { }) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${ mode}`)
}
}
}
match (
raw: RawLocation,
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
get currentRoute (): ?Route {
return this.history && this.history.current
}
init (app: any) {
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
this.apps.push(app)
if (this.app) {
return
}
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
beforeResolve (fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
afterEach (fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
onReady (cb: Function, errorCb?: Function) {
this.history.onReady(cb, errorCb)
}
onError (errorCb: Function) {
this.history.onError(errorCb)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.replace(location, onComplete, onAbort)
}
go (n: number) {
this.history.go(n)
}
back () {
this.go(-1)
}
forward () {
this.go(1)
}
getMatchedComponents (to?: RawLocation | Route): Array
{ const route: any = to
? to.matched
? to
: this.resolve(to).route
: this.currentRoute
if (!route) {
return []
}
return [].concat.apply([], route.matched.map(m => {
return Object.keys(m.components).map(key => {
return m.components[key]
})
}))
}
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location,
route: Route,
href: string,
normalizedTo: Location,
resolved: Route
} {
const location = normalizeLocation(
to,
current || this.history.current,
append,
this
)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
normalizedTo: location,
resolved: route
}
}
addRoutes (routes: Array
) { this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
}VueRouter
定义了一些属性和方法,我们先从它的构造函数看,当我们执行new VueRouter
的时候做了哪些事情,如下所示:constructor (options: RouterOptions = { }) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)let mode = options.mode || ‘hash’
this.fallback = mode === ‘history’ && !supportsPushState && options.fallback !== false
if (this.fallback) {mode = 'hash'
}
if (!inBrowser) {mode = 'abstract'
}
this.mode = modeswitch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${ mode}`)
}
}
}构造函数定义了一些属性,其中
this.app
表示根Vue
实例,this.apps
保存持有$options.router
属性的Vue
实例,this.options
保存传入的路由配置,this.beforeHooks
、this.resolveHooks
、this.afterHooks
表示一些钩子函数,我们之后会介绍,this.matcher
表示路由匹配器,我们之后会介绍,this.fallback
表示在浏览器不支持history.pushState
的情况下,根据传入的fallback
配置参数,决定是否回退到hash
模式,this.mode
表示路由创建的模式,this.history
表示路由历史的具体的实现实例,它是根据this.mode
的不同实现不同,它有History
基类,然后不同的history
实现都是继承History
。实例化
VueRouter
后会返回它的实例router
,我们在new Vue
的时候会把router
作为配置的属性传入,beforeCreate
混入的时候有这么一段代码:beforeCreate() {
if (isDef(this.$options.router)) {// ...
this._router = this.$options.router
this._router.init(this)
// ...
}
}
所以组件在执行
beforeCreate
钩子函数的时候,如果传入了router
实例,都会执行router.init
方法:
init (app: any) {
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
this.apps.push(app)
if (this.app) {
return
}
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
init
的逻辑很简单,它传入的参数是Vue
实例,然后存储到this.apps
中;只有根Vue
实例会保存到this.app
中,并且会拿到当前的this.history
,根据它的不同类型来执行不同逻辑,由于我们平时使用hash
路由多一些,所以我们先看这部分逻辑,先定义了setupHashListener
函数,接着执行了history.transitionTo
方法,它是定义在History
基类中,代码在src/history/base.js
:transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const route = this.router.match(location, this.current)
// …
}我们先不着急去看
transitionTo
的具体实现,先看第一行代码,它调用了this.router.match
函数:match (
raw: RawLocation,
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
实际上是调用了
this.matcher.match
方法去做匹配,所以接下来我们先来了解一下matcher
的相关实现。
- 总结:我们大致对
VueRouter
类有了大致了解,知道了它的一些属性和方法,同时了解到在组件的初始化阶段,执行到beforeCreate
钩子函数的时候会执行router.init
方法,然后又会执行history.transitionTo
方法做路由过渡,进而引出了matcher
的概念,接下来我们先研究一下matcher
的相关实现。
还没有评论,来说两句吧...