终于到了分析 vue-router的核心部分了,让我们来请出transitionTo这尊大佛(src/history/base.js)。
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)
})
}
}
)
}
这个函数设计的还是很清晰简单的,获得了我们之前分析过的route之后,直接调用confirmTransition方法:
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
const abort = err => {
// after merging https://github.com/vuejs/vue-router/pull/2771 we
// When the user navigates through history through back/forward buttons
// we do not want to throw the error. We only throw it if directly calling
// push/replace. That's why it's not included in isError
if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
})
} else {
warn(false, 'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort(new NavigationDuplicated(route))
}
const { updated, deactivated, activated } = resolveQueue(//就是获取了一个即将更新的record组,一个即将失活的record,一个将要激活的record
this.current.matched,
route.matched
)
const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,//VueRouter.brforeEach时已经注册了。是个引用类型,异步组件不受影响。
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)//返回了一个函数,这个函数已经处理了异步组件和同步组件
)
this.pending = route
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {//这里写的第三个参数就是我们平常写next,to就是我们写在next里的参数
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)//调用beforeRouteEnter钩子
const queue = enterGuards.concat(this.router.resolveHooks)////调用beforeResolve钩子
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb()
})
})
}
})
})
可以看到confirmTransition做了下面几件事情:
- 根据path将record分成了updated(即将更新的),deactivated(即将失活的),activated (即将激活的)3组。
- 将所有的钩子压入一个队列中,顺序为:
- beforerouteLeave
- beforeEach
- beforeRouteUpdate
- beforeEnter
- 解析异步组件和同步组件
- beforeRouteEnter
- beforeResolve
- afterEach
我们现在看下这些生命周期钩子的具体实现:
beforerouteLeave
相关代码:
extractLeaveGuards(deactivated)
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
function extractGuards (
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
const guards = flatMapComponents(records, (def, instance, match, key) => {//Dictionary<Component>,Dictionary<Vue>,RouteRecord,key
const guard = extractGuard(def, name)//返回守卫
if (guard) {
return Array.isArray(guard)
? guard.map(guard => bind(guard, instance, match, key))
: bind(guard, instance, match, key)
}
})
return flatten(reverse ? guards.reverse() : guards)
}
最后返回了一个守卫数组
beforeEach
相关代码:
this.router.beforeHooks
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
beforeEach在VueRouter里定义了相关赋值
beforeRouteUpdate
这个和beforerouteLeave一样
beforeEnter
这个直接从record中拿
activated.map(m => m.beforeEnter),=
处理异步组件
resolveAsyncComponents(activated)//返回了一个函数数组,这个函数已经处理了异步组件和同步组件
src/util/resolve-components.js
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {//activated
return (to, from, next) => {
let hasAsync = false
let pending = 0
let error = null
flatMapComponents(matched, (def, _, match, key) => {//Dictionary<Component>,Dictionary<Vue>,RouteRecord,key
// if it's a function and doesn't have cid attached,
// assume it's an async component resolve function.
// we are not using Vue's default async resolving mechanism because
// we want to halt the navigation until the incoming component has been
// resolved.
if (typeof def === 'function' && def.cid === undefined) {//处理异步组件
hasAsync = true
pending++
const resolve = once(resolvedDef => {
if (isESModule(resolvedDef)) {
resolvedDef = resolvedDef.default
}
// save resolved on async factory in case it's used elsewhere
def.resolved = typeof resolvedDef === 'function'
? resolvedDef
: _Vue.extend(resolvedDef)
match.components[key] = resolvedDef
pending--
if (pending <= 0) {
next()
}
})
const reject = once(reason => {
const msg = `Failed to resolve async component ${key}: ${reason}`
process.env.NODE_ENV !== 'production' && warn(false, msg)
if (!error) {
error = isError(reason)
? reason
: new Error(msg)
next(error)
}
})
let res
try {
res = def(resolve, reject)
} catch (e) {
reject(e)
}
if (res) {
if (typeof res.then === 'function') {
res.then(resolve, reject)
} else {
// new syntax in Vue 2.3
const comp = res.component
if (comp && typeof comp.then === 'function') {
comp.then(resolve, reject)
}
}
}
}
})
if (!hasAsync) next()
}
}
可以看到先判断组件是不是function,如果是,就当异步组件来处理,由于webpack import这种方式返回的是一个promise对象,所有最后按照Promise那一套来处理,假如访问的组件不是function,那么就当同步函数来处理,不管是异步组件还是同步组件,最后要调用next才能进入下个hook(等会就讲这个next)。
beoforeRouteEnter
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
function extractEnterGuards (
activated: Array<RouteRecord>,
cbs: Array<Function>,
isValid: () => boolean
): Array<?Function> {
return extractGuards(
activated,
'beforeRouteEnter',
(guard, _, match, key) => {
return bindEnterGuard(guard, match, key, cbs, isValid)
}
)
}
function extractEnterGuards (
activated: Array<RouteRecord>,
cbs: Array<Function>,
isValid: () => boolean
): Array<?Function> {
return extractGuards(
activated,
'beforeRouteEnter',
(guard, _, match, key) => {
return bindEnterGuard(guard, match, key, cbs, isValid)
}
)
}
function bindEnterGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string,
cbs: Array<Function>,
isValid: () => boolean
): NavigationGuard {
return function routeEnterGuard (to, from, next) {
return guard(to, from, cb => {
if (typeof cb === 'function') {
cbs.push(() => {
// #750
// if a router-view is wrapped with an out-in transition,
// the instance may not have been registered at this time.
// we will need to poll for registration until current route
// is no longer valid.
poll(cb, match.instances, key, isValid)
})
}
next(cb)
})
}
}
function poll (//在被套transition缓动模式下不一定能拿到组件,不断轮询直到拿到实例
cb: any, // somehow flow cannot infer this is a function
instances: Object,
key: string,
isValid: () => boolean
) {
if (
instances[key] &&
!instances[key]._isBeingDestroyed // do not reuse being destroyed instance
) {
cb(instances[key])
} else if (isValid()) {
setTimeout(() => {
poll(cb, instances, key, isValid)
}, 16)
}
}
关键就在这个poll方法,它通过settimeout自己调自己,防止在套transition组件的一些缓动模式下拿不到组件,直到拿到组件,并且传入了实例(vue),这就是为什么我们能在beforeRouteEnter的回调函数里能拿到实例的原因!而回到函数本身会在$nextTick中执行。
afterEach
const queue = enterGuards.concat(this.router.resolveHooks)
这个和beforeEach一样。
那现在东西我们都准备好了,放到了队列中,现在就是执行他们了:
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)//调用beforeRouteEnter钩子
const queue = enterGuards.concat(this.router.resolveHooks)////调用beforeResolve钩子
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb()
})
})
}
})
})
再加一个Iterator:
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {//这里写的第三个参数就是我们平常写next,to就是我们写在next里的参数
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
runQuene定义(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(queue[index], () => {//fn == Iterator
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
可以看到runQuene定义中的fn就是Iterator,回到Iterator的定义中,也就是说next不执行,runQueue的就无法调用下一个回调(也就是我们的准备好的队列中的下一个钩子)。而hook中的第三个参数就是我们平常hook中的next的形参,这就是我们平常要用next进入下一个钩子的原因!
回到confirmTransition 中,最后执行了onComplete(route),而在onComplete中就是执行了afterEach的所有回调函数和更新了URL。
总结:
路由切换都是在transitionTo做的,它做了如下几件事:
1. 根据path将record分成了updated(即将更新的),deactivated(即将失活的),activated (即将激活的)3组。
2. 将所有的钩子压入一个队列中,顺序为:
- beforerouteLeave
- beforeEach
- beforeRouteUpdate
- beforeEnter
- 解析异步组件和同步组件
- beforeRouteEnter
- beforeResolve
- afterEach
3. 判断异步组件的依据是其是否是函数。
4. beforeRouteEnter能在回调中拿到vm实例的原因是因为其中有个Poll方法一直在轮询,直到成功拿到vm并将vm作为参数传入回调函数中,因此我们能在回调函数中拿到vm。而beforeRouteEnter中的回调函数本事会在$nextTick中执行。
5. vue-router定义了一个“异步函数队列化执行的函数”,又定义了一个ITerator函数,只有我们调用了next方法,“异步函数队列化执行的函数”的步进器才会前进1步,才会执行我们的hook队列,这就是我们平常在hook中调用next的原因。