前置准备
使用说明文档:router.vuejs.org/zh/guide/#j…
源码下载地址:github.com/vuejs/vue-r…
分支:dev(不断更新中) 版本:3.5.2
思考点
我们在开始使用vue的时候,一定会接触到VueRouter,那同时我们可能会存在一些疑问点,比如:
- Vue.use(VueRouter) 、new VueRouter()等操作是在做什么事情?
- beforeRouterEnter中为什么获取不到Vue实例?
- 使用路由守卫时为什么一定要调用next方法?
- 为什么hash模式下打开localhost:8080会自动添加/#/?
- 一次完整导航解析的流程是什么样的?
- ...
当然,在我们长期使用后,肯定会有了深入的了解,接下来,我们会从源码角度去进行简单的说明。
基础使用
常用的路由功能
- 路由信息 $route
- 路由实例 $router
- 路由跳转 router.replace
- 视图渲染容器 router-view
- 路由导航 router-link
- 路由守卫 beforeEach...
下面这部分代码是我们对于VueRouter的基础使用示例
// route.ts
import Vue from 'vue'
import VueRouter from 'vue-router'
// 1.注册VueRouter插件 调用VueRouter.install方法
Vue.use(VueRouter)
// 2.创建router实例 传入对应配置
export default router = new VueRouter({
mode: 'hash',
routes:[
{ path: '/home', component: Home},
{ path: '/my', component: My}
]
})
// main.ts
import Vue from 'vue'
import router from './router'
import App from './App.vue'
// 3.在创建和挂载根实例时,将router实例作为参数传入到Vue中,使得整个应用都可使用该功能
new Vue({
router,
render: (h: (arg0: any) => any) => h(App)
}).$mount('#app')
<!-- App.vue -->
<template>
<div id="app">
<!-- 路由导航 -->
<router-link to="/home">首页</router-link>
<router-link to="/my">我的</router-link>
<!-- 视图渲染容器 -->
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
源码分析
插件注册
Vue.use(plugins)是vue注册插件的方法,该方法会检测插件是否有install方法,有则执行。
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
install源码解析
import View from './components/view'
import Link from './components/link'
// 声明vue 方便其他文件使用
export let _Vue
// 插件注册方法
export function install (Vue) {
// 避免多次注册插件
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
const isDef = v => v !== undefined
// 调用RouterView上绑定的data.registerRouteInstance方法
// 该方法为当前路由记录RouteRecord添加了对应的实例instance
// 即matched.instances[name] = callVal
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 全局混入beforeCreate和destroyed生命周期 为各组件绑定vue根实例
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
// 根组件 绑定_routerRoot为根实例
this._routerRoot = this
// 声明_router,指向VueRouter实例
this._router = this.$options.router
// 初始化VueRouter
this._router.init(this)
// vue根实例响应式绑定_route 也就是当前路由信息Route
// 此处的核心在于:router-view组件依赖了该属性,而该属性是响应式声明,所以在路由变化的时候,视图就会更新
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 非根组件 绑定_routerRoot为根实例
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 为当前路由记录RouteRecord添加了对应的实例instance
// *** 保证后续执行beforeRouteEnter中next回调函数时有实例可用
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// Vue原型初始化属性 $router(路由实例) $route(当前路由信息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)
const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats
}
VueRouter.init源码解析
init (app: any /* Vue component instance */) {
this.apps.push(app)
// 保证一个vue应用只初始化一次 app表示的是vue根实例
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)
}
}
// 开启路由监听 history模式监听popstate hash模式支持history则监听popstate,否则监听hashchange
const setupListeners = routeOrError => {
// 监听popState或者hashChange事件触发后 重新导航一次
history.setupListeners()
handleInitialScroll(routeOrError)
}
// 完成首次导航,并开启路由监听
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
// 调用history.listen方法,传入一个回调函数,该回调函数会在路由信息Route更新后执行
// 即在路由信息Route更新后会同步更新vue根实例上的_route
// _route属性被劫持,所以会通知相关依赖,其中包括RouterView组件,达到视图更新的效果
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
VueRouter实例
我们在创建VueRouter实例时,传入了一些配置,比如mode(路由模式)、routes(路由相关配置)等。那这些配置的实际用途是什么呢?接下来我们根据源码来了解下。
import VueRouter from 'vue-router'
export default router = new VueRouter({
mode: 'hash',
routes:[
{ path: '/home', component: Home},
{ path: '/my', component: My}
]
}
VueRouter实例源码解析
通过源码,我们可以看到在构造VueRouter实例时,我们会判断当前浏览器是否支持history模式,不支持则会回退到hash模式。同时,我们调用createMatcher方法创建路由映射表并返回matcher对象。
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)
// 处理路由模式 并创建对应模式的history实例
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}`)
}
}
}
}
matcher源码解析
createMatcher方法中调用createRouteMap方法,生成了pathList、pathMap、nameMap三个路由映射表。
- pathList 路径列表 示例:['/my','/my/name','/my/detail/:id']
- pathMap 路径与路由记录RouteRecord的映射 示例:{'/my':MyRoute,'/my/name':NameRoute}
- nameMap 路由名称与路由记录RouteRecord的映射 示例:{'my':MyRoute,'name':NameRoute} 所以我们在命名路由名称时保持一个不重复的原则
matcher中包含了四个属性
- match 根据目标跳转路由与当前路由信息Route,在路由映射表中进行匹配,并返回匹配到的路由信息Route
- addRoute 动态添加单个路由,即往pathList、pathMap、nameMap路由映射表中添加数据
- addRoutes 动态添加多个路由
- getRoutes 获取路由记录RouteRecord列表 返回示例:[MyRoute,NameRoute]
该示例为实际的路由记录RouteRecord对象结构
该示例为实际的路由信息Route对象结构
createRouteMap
创建路由映射表,维护pathList、pathMap、nameMap数据。
addRoute和addRoutes动态添加路由方法的核心就是createRouteMap,也就是往路由映射表里新增配置数据。
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>,
parentRoute?: RouteRecord
){
// 若是首次创建路由映射表则直接默认为空,若是后期动态添加路由则需要传入历史的路由映射表,以保证在原有基础上添加
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
// 遍历创建VueRouter实例时传入的配置信息routes 进行路由记录RouteRecord的添加
// 此处的route可能是嵌套路由即存在children,所以我们需要递归调用addRouteRecord
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})
// 确保通配符*路由一定在pathList的最后
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
生成路由记录RouteRecord并添加到pathList、pathMap、nameMap中。
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 若非一级路由则父子路由拼接成完整可访问path
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
// *** 路由记录RouteRecord
const record: RouteRecord = {
path: normalizedPath,
// 根据path解析出来的正则表达式扩展 在match方法中使用path匹配路由信息时用到
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
// router.ts中routes配置的路由对应渲染的组件
components: route.components || { default: route.component },
alias: route.alias
? typeof route.alias === 'string'
? [route.alias]
: route.alias
: [],
// 组件实例
instances: {},
// 收集beforeRouteEnter守卫中next的回调函数
enteredCbs: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
// 若存在子路由则递归调用addRouteRecord方法
// 从此处能看出来,我们是先递归添加子路由,再添加父路由,所以pathList的顺序是先子后父
if (route.children) {
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 若不存在子路由 则直接向pathList、pathMap、nameMap中添加数据
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 路由别名处理 根据别名生成对应路由映射
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 || '/'
)
}
}
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
}
}
match
macth方法根据参数raw(目标路由)以及currentRoute(当前路由信息Route)在nameMap或者pathMap中查找到对应的路由记录RouteRecord,调用_createRoute生成路由信息Route并返回。
raw是我们实际调用push或者replace等跳转路由方法传入的参数
形如'/my?param=123'或者是{name:'My',params:{param:123}}等。
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
// 根据入参生成格式化后的目标路径信息
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
// 我们会优先使用name去匹配 没有配置name再使用path匹配
if (name) {
const record = nameMap[name]
// 未匹配到路由 _createRoute第一个参数是匹配到的路由记录RouteRecord
// 此处传null最后生成对路由信息Route中的matched为[],也就是没有组件需要渲染,即页面空白
if (!record) return _createRoute(null, location)
// 获取所有必须的params。如果optional为true说明params不是必须的
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
if (typeof location.params !== 'object') {
location.params = {}
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
// path和params整合返回一个真实的路径
location.path = fillParams(record.path, location.params, `named route "${name}"`)
// 返回匹配的路由信息Route
return _createRoute(record, location, redirectedFrom)
} else if (location.path) {
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
// 使用路由记录RouteRecord的regex对目标路由进行匹配 若匹配则返回路由信息Route
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
// 未匹配到路由
return _createRoute(null, location)
}
看完match方法源码的解释,你是否会有一个疑问,为什么使用name直接去nameMap中匹配路由即可,而使用path匹配路由却需要遍历pathList呢?
这是因为我们配置中存在形如'/my/:id'的path路径,而这种路径在实际跳转时访问的是类似于'/my/123'带有实际参数的路径,如果我们直接使用pathMap去匹配是匹配不到的,所以我们需要遍历pathList,根据每一个path的正则以及目标路由的path和params去进行匹配,得到'/my/:id'对应的RouteRecord就是'/my/123'的对应的RouteRecord。
History
HashRouter实例
在创建HashRouter实例时,我们会检查url中是否包含/#/,没有则进行添加。同时HashRouter为我们提供了路由跳转能力,比如push、replace、go等,之后我们会一一说明。
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// 若是采用降级方案采用的hash模式 则会判断当前路径是否有/#/
// 没有则会添加,并使用window.location.replace切换路径
if (fallback && checkFallback(this.base)) {
return
}
// 没有采用降级方案,直接采用的hash模式
ensureSlash()
}
}
结合上下两部分代码,我们能看到,首次创建HashRouter实例时,会调用checkFallback或者ensureSlash来确保hash模式下url中有/#/。 同时,这也就解释了为什么hash模式下打开localhost:8080会自动添加/#/。
function ensureSlash (): boolean {
// 获取#后的字符串
const path = getHash()
// 若第一个字符是/ 则表示为正确的路由且有# 则不做处理
if (path.charAt(0) === '/') {
return true
}
// 若第一个字符是/ 则表示当前路由没有#或者#/字符
// 拼接好目标path 调用replaceHash来完成路径切换
replaceHash('/' + path)
return false
}
HistoryRouter实例
HistoryRouter和HashRouter实现基本一致,差异在于HistoryRouter不会做容错处理,不会判断是否支持historyApi,而是直接使用。
路由导航
上面的内容,都是在做路由能力的准备工作,接下来,我们将依托于这些准备工作,来分析路由导航的实现流程。
go、forward、back
这三个方法最终调用的都是window.history.go(n)。那么这个方法是如何更新的视图呢?还记得我们在分析插件注册时,有说过VueRouter.init完成了首次导航,并开启路由监听吗?这就是它们更新视图的原因。
我们监听了window上的popstate或者是hashchange事件,这样在路由变化时,就会调用transtionTo方法完成对_route路由信息的更新,从而触发视图渲染。所以,在这种情况下,浏览器地址变化是在视图更新前的。
go (n) {
this.history.go(n)
}
back () {
this.go(-1)
}
forward () {
this.go(1)
}
push、replace
push和replace方法的核心是类似的,也就是transitionTo,唯一不同之处在于导航切换完成后的回调中,push调用pushHash更新路由,replace调用replaceHash更新路由。
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
// 最后调用的是window.location.push或者history.pushState
pushHash(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 => {
// 最后调用的是window.location.replace或者history.replaceState
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
transitionTo
不管是浏览器地址变化,还是调用push、replace方法跳转,核心都在于transitionTo方法,接下来,我们将重点分析该方法。
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route
// 根据目标路由以及当前的路由信息Route调用match方法进行路由匹配
// 返回目标路由的Route对象
route = this.router.match(location, this.current)
// 调用confirmTransition
this.confirmTransition(
route,
() => {
// ... 成功回调
},
err => {
// ...失败回调
}
)
}
核心工具方法
首先,我们先分析整个流程中用到的核心方法,后续再分析流程时就不需要再打断思路了。
1.resolveQueue 该方法是我们根据传入的当前和目标路由RouteRecord来获取updated(需要更新的路由记录数组)、deactivated(需要销毁的路由记录数组)、activated(需要激活的路由记录数组)的。而这些数组是我们判断执行哪些路由守卫的依据。
function resolveQueue (
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
if (current[i] !== next[i]) {
break
}
}
// 结合代码与上面的图片,我们能清晰的看到,哪些需要更新,哪些需要销毁,哪些需要激活
return {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
}
2.extractLeaveGuards、extractUpdateHooks、extractEnterGuards
这几个方法是我们用来获取beforeRouteLeave、beforeRouteUpdate、beforeRouteEnter守卫的。
// 虽然在处理beforeRouteLeave、beforeRouteUpdate和beforeRouteEnter
// 都调用了extractGuards提取守卫,但是传入的参数是不一致的
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}
function extractEnterGuards (
activated: Array<RouteRecord>
): Array<?Function> {
return extractGuards(
activated,
'beforeRouteEnter',
(guard, _, match, key) => {
return bindEnterGuard(guard, match, key)
}
)
}
// 提取路由守卫
function extractGuards (
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
// 获取到所有的守卫 调用flatMapComponents遍历records,然后将参数传给回调函数
// def组件 instance组件实例 match路由记录RouteRecord数组 key组件名默认为default
const guards = flatMapComponents(records, (def, instance, match, key) => {
// 获取组件options下对应命名的路由守卫
const guard = extractGuard(def, name)
// 绑定this指向
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)
}
// beforeRouteLeave、beforeRouteUpdate调用该方法
function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
if (instance) {
return function boundRouteGuard () {
// 绑定this指向为组件实例
return guard.apply(instance, arguments)
}
}
}
// beforeRouteEnter调用该方法
function bindEnterGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string
): NavigationGuard {
return function routeEnterGuard (to, from, next) {
return guard(to, from, cb => {
// 执行beforeRouteEnter守卫时,我们判断next方法传入的参数是否为函数
// 若是函数 则将该回调函数维护在对应路由记录RouteRecord.enteredCbs中
if (typeof cb === 'function') {
if (!match.enteredCbs[key]) {
match.enteredCbs[key] = []
}
match.enteredCbs[key].push(cb)
}
next(cb)
})
}
为什么beforeRouterEnter守卫需要做特殊处理,且支持next传入回调函数呢?
众所周知,我们在该路由守卫中是没有办法获取到vue实例的,但我们是可以在next回调函数中获取到的。所以我们在提取beforeRouteEnter守卫时需要先将其next回调函数存储起来留作后续使用。
3.handleRouteRntered
批量处理beforeRouteEnter守卫中next传入的回调函数。
// route为目标路由信息
function handleRouteEntered (route) {
// 遍历目标路由匹配到的所有RouteRecord
for (var i = 0; i < route.matched.length; i++) {
var record = route.matched[i];
// 实例存在则处理
for (var name in record.instances) {
var instance = record.instances[name];
// 获取到我们之前在bindEnterGuard中存储的回调函数
var cbs = record.enteredCbs[name];
if (!instance || !cbs) { continue }
delete record.enteredCbs[name];
// 若回调函数存在 则调用,且传入instance也就是组件实例作为入参
// 所以这就是为什么我们能在beforeRouteEnter的next回调函数中获取到组件实例的原因
for (var i$1 = 0; i$1 < cbs.length; i$1++) {
if (!instance._isBeingDestroyed) { cbs[i$1](instance); }
}
}
}
}
4.runQueue
该方法将传入的路由守卫队列同步依次执行。
// queue路由守卫队列 fn迭代器 cb是队列执行完毕后执行的回调函数
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], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
// 迭代器 hook路由守卫函数 next进入下一个迭代的回调函数
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
// 路由守卫函数 目标路由信息 当前路由信息 next回调函数
// *** 假设我们在调用路由守卫时不执行next,那就导致我们没有调用runQueue的step方法
// 队列也就停止往后执行。所以这就是我们为什么一定要执行next的原因
hook(route, current, (to: any) => {
if (to === false) {
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
abort(createNavigationRedirectedError(current, route))
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// *** 调用next方法使得队列进入下一个迭代
next(to)
}
})
} catch (e) {
abort(e)
}
}
5.updateRoute
更新当前路由信息Route。
updateRoute (route: Route) {
// 更新当前路由信息
this.current = route
// 你是否还记得,在插件注册时调用了VueRouter.init方法,其中调用了history.listen
// 将其回调函数赋值给了cb 所以此处我们执行的就是app._route=route
// 由于_route是响应式声明的,所以会通知对应的依赖
// 其中包含RouterView,也就完成了视图的渲染更新
this.cb && this.cb(route)
}
// VueRouter
init(){
// ...
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
confirmTransition
1.判断是否为相同路由,若是则取消导航。
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()
return abort(createNavigationDuplicatedError(current, route))
}
2.调用resolveQueue方法,获取updated、deactivated、activated。
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
3.逐一提取路由守卫,并将他们合并到同一个队列queue中。
通过下面这段源码,我们能清晰的分析出来,各个路由守卫应该写在哪里
- 组件内部:beforeRouteLeave、beforeRouteUpdate
- VueRouter实例上:beforeEach
- 路由配置routes中:beforeEnter
const queue: Array<?NavigationGuard> = [].concat(
// 销毁组件的beforeRouteLeave守卫
extractLeaveGuards(deactivated),
// 全局beforeEach守卫
this.router.beforeHooks,
// 更新组件的beforeRouteUpdate守卫
extractUpdateHooks(updated),
// 激活组件的beforeEnter守卫
activated.map(m => m.beforeEnter),
// 异步加载的激活组件
resolveAsyncComponents(activated)
)
4.调用runQueue方法顺序依次执行queue队列。
// 传入执行队列、迭代器、成功回调参数
// 将队列中每一个路由守卫函数传给迭代器,在迭代器中执行路由守卫
// 并且路由守卫中必须调用next方法,队列才会进入下一个迭代
// 迭代完成后,调用该成功回调
runQueue(queue, iterator, () => {
// 激活组件可能包含需要异步加载的,为了保证获取到所有激活组件beforeRouteEnter守卫
// 我们在第一个队列迭代完成后,再开启一个新的队列进行迭代
const enterGuards = extractEnterGuards(activated)
// beforeRouterEnter、beforeResolve路由守卫
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
// 执行成功回调 更新当前路由信息Route
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
// 执行beforeRouteEnter路由守卫中next传入的回调函数
handleRouteEntered(route)
})
}
})
})
5.两个队列迭代完成后,调用confirmTransition的成功回调函数。
this.confirmTransition(
route,
() => {
// 更新路由信息 触发视图更新
this.updateRoute(route)
// 调用transitionTo回调函数:
// 即调用pushHash/replaceHash方法切换路由,执行push传入的成功回调函数
onComplete && onComplete(route)
this.ensureURL()
// 执行afterEach路由守卫
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
// ...
}
})
流程图
至此,我们就把push或者replace的流程说完了。现在我们来回顾下push方法的整个调用流程,如图:
push和replace是先更新的_route及视图,再更新的浏览器地址;而go是先更新的浏览器地址,再更新的_route及视图。
导航解析完成流程
beforeRouteEnter中获取不到实例原因解析:
beforeRouteEnter路由守卫在视图更新前执行,registerInstance方法声明在RouterView,而我们调用registerInstance方法是在视图更新后,beforeCreate中。
所以我们在beforeRouteEnter中肯定没有办法获取到实例instance。而执行beforeRouteEnter路由守卫中next回调是在$nextTick中,也就是异步执行,这个时候beforeCreate已经执行完毕,所以此时可以获取到实例instance。
总结
至此,VueRouter源码部分已经大致分析完成,最后梳理出一张思维导图供给大家参考。当然vue3现在也已经走入我们的视野,相应的VueRouter版本也进行了升级,使用方法上也有了一定的变化,感兴趣的可以做下两个版本的比较和学习。