今天来分析一下vue-router的部分源码,本篇主要分析hash模式的push。
URL语法通用格式
直观感受
hash模式在路由改变时,只会改变#后面的这个字符串,也就是location.hash,此时页面不会真正地刷新,但页面的确发生了渲染。
槽点:身为一个hybrid app,链接丑成啥样都不要紧,但客户端和服务端都得注意要对他们的h5链接拼接函数做健壮性修复。
调试
拿一个熟悉的项目打断点调试
vue.use
使用
import Router from 'vue-router';
Vue.use(Router);
const Router=new Router({})
源码
- Vue.use
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
}
如果这个插件有提供install方法,则调用它;否则,调用这个plugin。
- vue-router提供的install方法如下:
粗略地看,在这个函数里使用了Vue.mixin;在Vue.prototype挂了$router
、$route
;注册了两个组件RouterView、RouterLink
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
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
值得一提的是,使用Vue.mixin全局混入,在beforeCreate这个钩子干了十分关键的事情
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
- 将new Vue时传入的router实例赋值给_router
- 调用init
init (app: any /* Vue component instance */) {
……
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
})
})
}
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
() => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
)
}
这个transitionTo入参为
transitionTo ( location: RawLocation, onComplete?: Function, onAbort?: Function )
可以看到,不管是哪个模式,都会执行transitionTo,如果判定为hash模式,onComplete结束回调为setupListeners。也就是说,添加一个popstate事件的监听,这个事件被触发之后,也会调用transitionTo
-
Vue.util.defineReactive(this, '_route', this._router.history.current)
将_route用 Vue.util.defineReactive 变为响应式对象。这样,当它的值发生变更时,将触发render()函数从而触发页面的渲染动作。
transitionTo
这个无处不在的transitionTo就是处理跳转的关键函数
- transitionTo
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
const route = this.router.match(location, this.current)
this.confirmTransition(
route,
() => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
)
}
使用router.match得到了一个route对象如下:
之后调用了confirmTransition,在这个函数中,主要是走导航守卫的逻辑
然后执行传给它的onComplete
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
- updateRoute
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
保存当前的路由this.current用以调用他的afterHooks,并设置this.current为即将跳转的route。然后调用this.cb。
- this.cb
这个cb在History的listen函数里被设置上,也就是init函数里。
app为init传入的Vue component instance
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
干的事情就是对当前的每一个Vue component instance都提供这个_route。之前我们提到过,如果修改了_route,由于这个对象为vue的响应式对象,render函数会被调用以触发页面渲染。
push
- push
调用了$router.push,就是
$router.push->VueRouter.prototype.push->HashHistory.prototype.push
hash.js 的 push
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
// preserve existing history state as it could be overriden by the user
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
可以看到push给this.transitionTo传入的结束回调里,pushHash会先调用getUrl更新#后面的字符串,得到新的链接,然后再传给pushState。最终会调用history.pushState来修改历史栈以保证浏览器前进返回行为像真正的跳转那样表达。
帮助理解的流程图
——in 在梅雨里的七月中旬