对于单页应用来说,很重要的一点,它可以被称之为应用,则是因为它内部的跳转不是传统网页的刷新跳转,而是无刷新跳转(网上也称这种跳转方式的实现为 Pjax)。并且,对于 Vue.js 来说,它本身仅仅是提供了数据驱动的能力,它的其他能力则是通过 Vue.use() 来安装注册插件来实现能力的扩展。
Pjax 的优点:
这些优点,对于使用过 Vue-Router 的同学来说,应该都体验过。那么接下来我们看看 Vue-Router 实现注册和初始化(考虑到这个过程牵扯的东西比较多,所有整个 Vue-Router 的实现会分多篇博客讲解)
首先,对于 Vue.js 而言,每一个插件都需要通过 Vue.use() 方法进行注册,Vue.use 定义在 src/core/global-api/use.js
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
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 中所有的插件都被保存在 installedPlugins 中,并且通过 installed.plugins.index() 来防止重复注册,并且关键的一点就是,通过 args.unshift() 方法来将 Vue 实例加入到插件的 arguments 中,方便插件使用 Vue 的一些能力。最终会执行插件的 install 方法或本身(如果为函数)来实现插件。
对于 Vue-Router 而言,install 方法是这样的
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 () {
// 如果此时是根 Vue 实例
if (isDef(this.$options.router)) {
// 将根实例赋值给 _routerRoot
this._routerRoot = this
// 获取根实例上的 router(这个就是我们初始化的 VueRouter 实例)
this._router = this.$options.router
// 调用 init 方法
this._router.init(this)
// 定义 _route 属性为根 Vue 实例上可响应的,值为 this._router.history.current
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else { // 如果此时不是根 Vue 实例
// 获取它的 parent(它肯定是根 Vue 实例)
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
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
可以看出,在 install 方法中,也同样对自己的调用进行了判断,保证 install 方法只执行一次。并且,install 方法调用了 Vue.tr() 方法,在 beforeCreate(创建)和 destoryed 方法中执行了相应的方法。然后,在 Vue 实例上定义了 $router 属性,很明显 $router 属性只是只读的。
从注册的代码中可以看出,如果此时是根实例,则会走 init 这个方法,当然在此之前 constructor 中的代码肯定是执行完毕的。
constructor
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// createMatcher() 会返回一个对象
// 这个对象是这样的 {pathList, pathMap, nameMap}
// pathList 存了路由中的所有路径
// pathMap 存了
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}`)
}
}
}
其实 constructor 的逻辑很简单,即根据传入的配置初始化 History,除开服务端渲染的 abstract,默认或不支持 History 则走 Hash。 并且,其中 createMatcher 方法会为路由建立映射和路由
createMatcher
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// the path list is used to control path matching priority
const pathList: Array<string> = oldPathList || []
// $flow-disable-line
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
routes.forEach(route => {
// 为 pathList pathMap nameMap 添加记录
addRouteRecord(pathList, pathMap, nameMap, route)
})
// ensure wildcard routes are always at the end
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
...
return {
pathList,
pathMap,
nameMap
}
}
createMatcher 方法的作用就创建一个 Matcher,暴露三个属性,其中 pathList 用于存储路由的 path,pathMap 和 nameMap 用于路由的 path 或 name 到 record 的映射,record 的结构是这样的:
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
init
init (app: any /* Vue component instance */) {
...
this.apps.push(app)
// set up app destroyed handler
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// clean out app from this.apps array once destroyed
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
})
// main app previously initialized
// return as we don't need to set up new history listener
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
})
})
}
此时 app 则是我们在注册的时候,传入的根 Vue 实例(即也可以理解为带 router 选项的实例),将根 Vue 实例 push 到数组 apps 中,通过 Vue 实例的能力 $once 来监听 destoryed 事件,该事件的作用在于组件 destoryed 时,移除 apps 数组中保存的 app(改组件的实例,需要注意的是这种组件也是需要具备 router 选项,否则就是根实例),并且最后走了一个逻辑,当前的 app(即 this.app) 等于 destory 中移除的 app 时,此时需要令 this.app 等于 apps 数组中的第一个或者是 null,当前至少存在一个 Router,以便复用。