本次阅读的
vue-router源码版本是3.5.3
0.目录结构
compoents下是自带的两个组件<route-link><route-view>
history下是根据不同的mode来创建不同的路由实例util是工具函数
1.入口
index.js
export default class VueRouter {}
VueRouter.install = install
VueRouter.version = '__VERSION__'
VueRouter.isNavigationFailure = isNavigationFailure
VueRouter.NavigationFailureType = NavigationFailureType
VueRouter.START_LOCATION = START
1. Vue.use(VueRouter)
一下是官方给出的简单使用范例
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 路由表
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
// 创建路由实例
const router = new VueRouter({
routes
})
const app = new Vue({
router
}).$mount('#app')
Vue.use(VueRouter)是执行他的VueRouter.install方法,在之前的源码阅读中已说明,这里不作赘述
install
- 防止重复注册
Vue.mixin混入beforeCreate和destroyed全局钩子- 代理
vue实例上的$router和$route - 注册组件
<route-link>和<route-view> - 获取到
vue的合并策略,给vue-router自己的钩子函数,这样在Vue初始化过程mergeOptions过程中会合并路由守卫钩子
// 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)
}
}
// 混入全局 beforeCreate,destoryed
Vue.mixin({
beforeCreate () {
// 根组件创建的时候会传router
if (isDef(this.$options.router)) {
// vm实例
this._routerRoot = this
// 这里是 new VueRouter的实例
this._router = this.$options.router
// 用该实例的init方法初始化,后面分析改方法
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)
}
})
// 代理$router,$route
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)
// 注册合并策略,这里就会用vue内置的created的合并策略去合并vue-router内置的钩子
const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter
= strats.beforeRouteLeave
= strats.beforeRouteUpdate
= strats.created
}
2. new VueRouter()
VueRouter类
该类声明在index.js中,且它正是默认导出
构造函数
- 声明初始化内部变量
mode根据环境做调整- 根据
mode值创建相关实例挂载到this.history上
export default class VueRouter {
// ...
constructor (options: RouterOptions = {}) {
// 初始化内部变量
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
// 对mode做处理,默认hash
let mode = options.mode || 'hash'
// 浏览器不支持pushState时对history做退化处理
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
// 不是浏览器环境
if (!inBrowser) {
mode = 'abstract'
}
this.mode = 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}`)
}
}
}
/* install 方法会调用 init 来初始化 */
init(app: any /* Vue组件实例 */) { }
/* createMatcher 方法返回的 match 方法 */
match(raw: RawLocation, current?: Route, redirectedFrom?: Location) { }
/* 当前路由对象 */
get currentRoute() { }
/* 注册 beforeHooks 事件 */
beforeEach(fn: Function): Function { }
/* 注册 resolveHooks 事件 */
beforeResolve(fn: Function): Function { }
/* 注册 afterHooks 事件 */
afterEach(fn: Function): Function { }
/* onReady 事件 */
onReady(cb: Function, errorCb?: Function) { }
/* onError 事件 */
onError(errorCb: Function) { }
/* 调用 transitionTo 跳转路由 */
push(location: RawLocation, onComplete?: Function, onAbort?: Function) { }
/* 调用 transitionTo 跳转路由 */
replace(location: RawLocation, onComplete?: Function, onAbort?: Function) { }
/* 跳转到指定历史记录 */
go(n: number) { }
/* 后退 */
back() { }
/* 前进 */
forward() { }
/* 获取路由匹配的组件 */
getMatchedComponents(to?: RawLocation | Route) { }
/* 根据路由对象返回浏览器路径等信息 */
resolve(to: RawLocation, current?: Route, append?: boolean) { }
/* 动态添加路由 */
addRoutes(routes: Array<RouteConfig>) { }
}
我们知道初始化实例的时候routes路由表是最重要的东西,在构造函数中通过creareMatcher函数创建了一个matcher挂载到了实例属性matcher上,其实这个时候routes已经通过闭包的方式保存了,下面深入到createMatcher
this.matcher = createMatcher(options.routes || [], this)
createMatcher 实际就声明了一些方法然后将四个方法暴露出来,通过createRouteMap将routes路由表转化后闭包保存,所以继续深入到createRouteMap
// src/create-matcher.js
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {}
function addRoute (parentOrRoute, route) {}
function getRoutes () {}
function match (){}
function redirect (){}
function alias (){}
function _createRoute (){}
return {
match,
addRoute,
getRoutes,
addRoutes
}
}
createRouteMap
- 保存更新前状态
- 迭代路由表用
addRouteRecord构造路由,所以继续深入到addRouteRecord *通配符匹配路由放到最后- 返回
pathList、pathMap、nameMap
// src/create-route-map
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>,
parentRoute?: RouteRecord
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// 兼容更新前状态
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
// 迭代路由表
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})
// 确保通配符匹配的路由放到路由表的最后
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
}
}
addRouteRecord
- 标准化
path - 创建当前的路由对象
record,这里动态路由匹配的情况需要创建一个正则表达式,这里vue-router使用 path-to-regexp (opens new window)作为路径匹配引擎,所以支持很多高级的匹配模式,这里不做深入探讨 - 递归创建嵌套的子路由
- 注册
pathMap和pathList - 注册别名路由
- 注册
nameMap
我们知道push和replace的编程式导航中的第一个参数location对象可以是name或path来跳转到指定路由,那么分别对应了nameMap和pathMap两个映射表
function addRouteRecord (
// 全局的路由映射
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
// 当前添加的路由对象
route: RouteConfig,
// 当前添加的路由的父路由
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
// 编译正则匹配的配置
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
// 标准化path
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// 匹配大小写是否敏感
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
// 创建路由对象
const record: RouteRecord = {
path: normalizedPath,
// 正则动态路由匹配
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
// 命名视图组件
components: route.components || { default: route.component },
// 别名 可以是字符串或者是字符串数组
alias: route.alias
? typeof route.alias === 'string'
? [route.alias]
: route.alias
: [],
instances: {},
enteredCbs: {},
name,
parent,
matchAs,
// 重定向路由
redirect: route.redirect,
// 路由守卫
beforeEnter: route.beforeEnter,
// 路由元信息
meta: route.meta || {},
// 路由props
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
// 嵌套路由递归调用
if (route.children) {
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
// 第五个参数是parent,指向当前record
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 看到这里我们明白了,原来pathMap是保存path到路由对象的映射的
if (!pathMap[record.path]) {
// 这个数组相当于是Object.keys(pathMap)
pathList.push(record.path)
pathMap[record.path] = record
}
// 注册别名
// 别名可以让路由的配置不局限于嵌套的路由,可以规范化url的配置
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
for (let i = 0; i < aliases.length; ++i) {
const alias = aliases[i]
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
}
}
// nameMap是路由表name到record的映射
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
}
}
import Regexp from 'path-to-regexp'
function compileRouteRegex (
path: string,
pathToRegexpOptions: PathToRegexpOptions
): RouteRegExp {
const regex = Regexp(path, [], pathToRegexpOptions)
return regex
}
function normalizePath (
path: string,
parent?: RouteRecord,
strict?: boolean
): string {
if (!strict) path = path.replace(/\/$/, '')
if (path[0] === '/') return path
if (parent == null) return path
return cleanPath(`${parent.path}/${path}`)
}
init方法
前面我们知道在install中的Vue.mixin的beforeCreate钩子中执行了该方法
- 维护
apps属性,是一个vue实例数组,注册时添加,vue实例销毁时删除 - 防止重复调用
init - 跳转到
history.getCurrentLocation()然后调用setupListeners,后面分析history实例的时候会讲到该方法的实现
init (app: any /* Vue component instance */) {
// app实际是vue实例
this.apps.push(app)
app.$once('hook:destroyed', () => {
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
if (this.app === app) this.app = this.apps[0] || null
if (!this.app) this.history.teardown()
})
// 防止重复初始化
if (this.app) {return}
this.app = app
const history = this.history
if (history instanceof HTML5History || history instanceof HashHistory) {
const handleInitialScroll = routeOrError => {
const from = history.current
const expectScroll = this.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll && 'fullPath' in routeOrError) {
handleScroll(this, routeOrError, from, false)
}
}
const setupListeners = routeOrError => {
history.setupListeners()
handleInitialScroll(routeOrError)
}
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
编程式导航
官方文档中给出的编程式导航就是该实例的三个方法
router.push(location, onComplete?, onAbort?)router.replace(location, onComplete?, onAbort?)router.go(n)当我们这三个API的实现实际上是依赖于不同mode下创建的history实例的API来的,所以我们研究这三个API还是得深入到各个类(AbstractHistory,HashHistory,HTML5History)中的方法,以下贴出VueRouter中的原型方法
push
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}
replace
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.replace(location, resolve, reject)
})
} else {
this.history.replace(location, onComplete, onAbort)
}
}
go
go (n: number) {
this.history.go(n)
}
HTML5History类
构造函数
构造函数中初始化了_startLocation
export class HTML5History extends History {
_startLocation: string
constructor (router: Router, base: ?string) {
super(router, base)
this._startLocation = getLocation(this.base)
}
setupListeners () {}
go (n: number) {}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {}
ensureURL (push?: boolean) {}
getCurrentLocation (): string {}
}
export function getLocation (base: string): string {
let path = window.location.pathname
const pathLowerCase = path.toLowerCase()
const baseLowerCase = base.toLowerCase()
if (base && ((pathLowerCase === baseLowerCase) ||
(pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {
path = path.slice(base.length)
}
// 这里的path去掉base
return (path || '/') + window.location.search + window.location.hash
}
setupListeners
该函数主要做的工作是监听了popstate事件(popstate - Web API 接口参考 | MDN (mozilla.org)),history.pushState和history.replaceState不会触发该事件,所以该事件实际上是为了实现监听用户点击浏览器的回退前进按钮的
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
// 看下面setupScroll的实现,讲解push和replace会解释为什么要全局维护这个key
// 因为它和每个页面的position信息相关
if (supportsScroll) {
this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
const current = this.current
// 这里会获取到当前的地址栏url
const location = getLocation(this.base)
// 有些浏览器在第一次进入就触发popState事件,这不是我们想要的
if (this.current === START && location === this._startLocation) {
return
}
// 跳转到url
this.transitionTo(location, route => {
if (supportsScroll) {
// 处理滚动
handleScroll(router, route, current, true)
}
})
}
window.addEventListener('popstate', handleRoutingEvent)
this.listeners.push(() => {
window.removeEventListener('popstate', handleRoutingEvent)
})
}
// src/util/scroll.js
export function setupScroll () {
if ('scrollRestoration' in window.history) {
window.history.scrollRestoration = 'manual'
}
const protocolAndPath = window.location.protocol + '//' + window.location.host
const absolutePath = window.location.href.replace(protocolAndPath, '')
const stateCopy = extend({}, window.history.state)
stateCopy.key = getStateKey()
window.history.replaceState(stateCopy, '', absolutePath)
window.addEventListener('popstate', handlePopState)
return () => {
window.removeEventListener('popstate', handlePopState)
}
}
function handlePopState (e) {
saveScrollPosition()
if (e.state && e.state.key) {
setStateKey(e.state.key)
}
}
在此顺便说一下这个this.listeners.push()什么时候会调用,看History父类定义的teardown,像是一个简单的发布订阅模式,但是我们看一下这个数组中存到函数都是做什么的?
teardown () {
this.listeners.forEach(cleanupListener => {
cleanupListener()
})
this.listeners = []
this.current = START
this.pending = null
}
还记得在init初始化中调用了teardown,相当于当所有的注册的vue实例都销毁了那么就把当前vuerouter实例也重置了,将监听事件取消,所以listeners中的函数都是取消监听的函数
init(app){
app.$once('hook:destroyed', () => {
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
if (this.app === app) this.app = this.apps[0] || null
if (!this.app) this.history.teardown()
})
}
push、replace、go
push和replace唯一的不同就是一个调用的replaceState一个是pushState
import { pushState, replaceState, supportsPushState } from '../util/push-state'
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 当前路由
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 当前路由
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
// 会触发popState事件
go (n: number) {
window.history.go(n)
}
下面看一下pushState和replaceState的实现,主要是用的原生history的pushState和replaceState的两个API,这个两个API特点是会修改浏览器地址栏内容,但是不会发出实质的请求,而且会修改页面栈,这也history模式实现的关键。
这两个原生API的使用方法
- History.pushState() - Web API 接口参考 | MDN (mozilla.org)
- History.replaceState() - Web API 接口参考 | MDN (mozilla.org)
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
const history = window.history
try {
if (replace) {
// 这个状态用户可能原来就有东西,我们只是在原来的对象上增加一个唯一的key值
// 所以需要先将原来的state进行拷贝
const stateCopy = extend({}, history.state)
// replace时key值不变
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
// push需要改变key值
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
// 异常处理用localtion来强制跳转
// 因为有些浏览器可能会限制页面栈数量
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
// 通过key值来map滚动状态,这个key值经过模块化封装是全局唯一的
export function saveScrollPosition () {
const key = getStateKey()
if (key) {
positionStore[key] = {
x: window.pageXOffset,
y: window.pageYOffset
}
}
}
// src/util/state-key.js
export function genStateKey (): string {
return Time.now().toFixed(3)
}
let _key: string = genStateKey()
export function getStateKey () {
return _key
}
export function setStateKey (key: string) {
return (_key = key)
}
transitionTo
push和replace方法都是基于transitionTo方法,这个方法定义在其父类History上
match到需要跳转的路由对象this.confirmTransition,继续深入这个方法
// src/history/base.js
transitionTo (location: RawLocation,onComplete?: Function,onAbort?: Function) {
let route
try {
// 通过location来匹配到我们要跳转的路由
route = this.router.match(location, this.current)
} catch (e) {
// 执行异常回调函数
this.errorCbs.forEach(cb => {
cb(e)
})
throw e
}
// 保存跳转前路由
const prev = this.current
this.confirmTransition(
route,
() => {
this.updateRoute(route)
// 执行用户传入的回调
onComplete && onComplete(route)
this.ensureURL()
// 执行全局后置钩子,对应下面提到的(10)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
if (onAbort) {
// 执行用户的回调
onAbort(err)
}
if (err && !this.ready) {
if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
}
)
}
confirmTransition
这个函数中前面是一对const的声明定义,最后执行了一个runQueue,我们可以先看一下runQueue的实现,可以看我的注释中说明了,实际上是一个迭代器,又因为传入的fn实际上调用了vue的$nextTick使得这个队列实际上是异步执行的,这也就说明了为什么定义runQueue方法的文件名叫async.js
runQueue
// src/util/async.js
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
// 队列执行完毕,执行回调函数
if (index >= queue.length) {
cb()
} else {
// 队列依次执行
if (queue[index]) {
// 用传入的fn执行,执行完毕后执行队列的下一个
fn(queue[index], () => {
step(index + 1)
})
} else {
// 队列中元素undefined直接跳过
step(index + 1)
}
}
}
step(0)
}
先看一下官方给出的完整导航解析流程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave守卫。 - 调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫 (2.2+)。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter。 - 调用全局的
beforeResolve守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
从runQueue的三个参数来分析
queue:其实是一堆钩子依次执行,我们发现resolveQueue函数的返回值中可以解构出updated,deactivated,activated,换句话说我们通过比较当前路由和目标路由的matched才能确定到底该执行哪些钩子,因为这两个路由有可能存在嵌套关系,这样一来我们需要决定该执行哪些组件钩子的beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave,可以看下面对该函数的注释分析extractLeaveGuards(deactivated):对应上述(2)this.router.beforeHooks:对应上述(3)extractUpdateHooks(updated):对应上述(4)activated.map(m => m.beforeEnter):对应上述(5)resolveAsyncComponents(activated):对应上述(6)
fn:就是一个迭代器,在该迭代器中包裹try...catch来使用abort回调,执行hook(to,from,next),且主要对next参数做了一些判断处理cb:在hook迭代结束后最后执行,在这里重新执行了两组钩子,执行完成后执行完成后的回调函数,也就是在这个时候才真正的pushState或replaceState了extractEnterGuards(activated):对应上述(7)this.router.resolveHooks:对应上述(8)
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
// 当要跳转的路由和当前一样
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
if (
isSameRoute(route, current) &&
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
// 重置路由
this.ensureURL()
// 重置滚动行为
if (route.hash) {
handleScroll(this.router, current, route, false)
}
return
}
// 判断执行哪些组件内的钩子
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
const queue: Array<?NavigationGuard> = [].concat(
// 当前组件失活钩子
extractLeaveGuards(deactivated),
// 全局路由前置钩子,通过router.beforeEach注册
this.router.beforeHooks,
// 组件更新钩子
extractUpdateHooks(updated),
// 组件激活钩子
activated.map(m => m.beforeEnter),
// 全局resolve钩子
resolveAsyncComponents(activated)
)
// 为了代码清晰我删除了一些abort的回调
const iterator = (hook: NavigationGuard, next) => {
// ...
try {
// 这里对应着钩子定义的to,from,next参数
hook(route, current, (to: any) => {
if (to === false) {
// 这意味着终止跳转
this.ensureURL(true)
} else if (isError(to)) {
this.ensureURL(true)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// 下一个钩子
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 第三个是钩子全都执行完毕之后的回调
runQueue(queue, iterator, () => {
// 组件激活钩子对应上述(7)
const enterGuards = extractEnterGuards(activated)
// 全局解析后守卫对应上述(8)
const queue = enterGuards.concat(this.router.resolveHooks)
// 对这两组钩子再进行循环
runQueue(queue, iterator, () => {
// 终于完成了,执行完成的回调函数
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
})
})
}
function resolveQueue (
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
// 要找到两个matched数组的最大值
const max = Math.max(current.length, next.length)
// 对匹配到的全部路径进行迭代
for (i = 0; i < max; i++) {
// 一旦匹配到不一致跳出循环,这也就意味着i记录着两个路由最后分道扬镳的路口
if (current[i] !== next[i]) {
break
}
}
return {
// 也就是说前面相同的部分是更新
updated: next.slice(0, i),
// 后面不同的部分是第一次进入
activated: next.slice(i),
// 当前后面的部分即将失活
deactivated: current.slice(i)
}
}
HashHistory类
同样继承自History类,且实现了其声明的接口
构造函数
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
if (fallback && checkFallback(this.base)) {
return
}
// 初始化跳转
ensureSlash()
}
}
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
export function getHash (): string {
let href = window.location.href
const index = href.indexOf('#')
if (index < 0) return ''
href = href.slice(index + 1)
return href
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
setupListeners
基本和history模式差不多,当popState不支持的时候用hashChange事件代替
setupListeners () {
if (this.listeners.length > 0) {
return
}
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
this.listeners.push(setupScroll())
}
const handleRoutingEvent = () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
const eventType = supportsPushState ? 'popstate' : 'hashchange'
window.addEventListener(
eventType,
handleRoutingEvent
)
this.listeners.push(() => {
window.removeEventListener(eventType, handleRoutingEvent)
})
}
push、replace、go
和history基本一样,也就是最后用的API在不支持pushState的时候换了一下
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
3.总结
主要有以下类
VueRouterhistory:在构造函数中创建了相应模式的对象matcher:闭包方式存储了routes路由表,和返回了三个方法match:就是调用matcher.matchinit:在Vue钩子beforeCreate中调用,注册popstate或hashchange事件,注册vue实例push、replace、go:实际上都是在调用history的方法back、forward:go的“语法糖”install(静态方法):Vue混入、注册组件、注册组件钩子、代理$router和$route对象
History: 作为HTML5History、HashHistory、AbstractHistory的父类transitionTo:获取到匹配到的路由,调用confirmTransitionconfirmTransition:调用不同的钩子,执行push或replace的回调
HTML5HistorysetupListeners:监听popstate事件push:pushStatereplace:replaceStatego:window.history.go
HashHistory:同上setupListeners:监听hashchange事件push:window.location.hashreplace:window.location.replacego:window.history.go
AbstractHistorypushreplacego
至于router.matcher中实现的方法有时间再做深入分析
因为笔者是边看边写的笔记,所以看的肯定是不足的望大佬指正,下一篇会再看
Vue-Router内置的两个组件~