一、 matcher
matcher
相关的实现都在src/create-matcher.js
中,我们先来看一下matcher
的数据结构:
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
};
Matcher
返回了两个方法,match
和addRoutes
,在之前我们接触到了match
方法,顾名思义它是做匹配,那么匹配的是什么,在介绍之前,我们先了解路由中重要的两个概念,Loaction
和Route
,它们的数据结构定义在flow/declarations.js
中,如下所示:
Location
declare type Location = {
_normalized?: boolean;
name?: string;
path?: string;
hash?: string;
query?: Dictionary<string>;
params?: Dictionary<string>;
append?: boolean;
replace?: boolean;
}
Vue-Router 中定义的
Location
数据结构和浏览器提供的window.location
部分结构有点类似,它们都是对url
的结构化描述。举个例子:/abc?foo=bar&baz=qux#hello
,它的path
是/abc
,query
是{foo:'bar',baz:'qux'}
。Location
的其他属性我们之后会介绍。
Route
declare type Route = {
path: string;
name: ?string;
hash: string;
query: Dictionary<string>;
params: Dictionary<string>;
fullPath: string;
matched: Array<RouteRecord>;
redirectedFrom?: string;
meta?: any;
}
Route
表示的是路由中的一条线路,它除了描述了类似Loctaion
的path
、query
、hash
这些概念,还有matched
表示匹配到的所有的RouteRecord
。Route
的其他属性我们之后会介绍。
createMatcher
,在了解了Location
和Route
后,我们来看一下matcher
的创建过程:
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
const record = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
warn(record, `Route with name '${name}' does not exist`)
}
if (!record) return _createRoute(null, location)
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]
}
}
}
if (record) {
location.path = fillParams(record.path, location.params, `named route "${name}"`)
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]
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
return _createRoute(null, location)
}
// ...
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoutes
}
}
createMatcher
接收两个参数,一个是router
,它是我们new VueRouter
返回的实例,一个是routes
,它是用户定义的路由配置,来看一下我们之前举的例子中的配置:
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
createMathcer
首先执行的逻辑是const { pathList, pathMap, nameMap } = createRouteMap(routes)
创建一个路由映射表,createRouteMap
的定义在src/create-route-map
中:
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<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)
})
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
}
}
createRouteMap
函数的目标是把用户的路由配置转换成一张路由映射表,它包含三个部分,pathList
存储所有的path
,pathMap
表示一个path
到RouteRecord
的映射关系,而nameMap
表示name
到RouteRecord
的映射关系。那么RouteRecord
到底是什么,先来看一下它的数据结构:
declare type RouteRecord = {
path: string;
regex: RouteRegExp;
components: Dictionary<any>;
instances: Dictionary<any>;
name: ?string;
parent: ?RouteRecord;
redirect: ?RedirectOption;
matchAs: ?string;
beforeEnter: ?NavigationGuard;
meta: any;
props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
}
它的创建是通过遍历
routes
为每一个route
执行addRouteRecord
方法生成一条记录,来看一下它的定义:
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
if (process.env.NODE_ENV !== 'production') {
assert(path != null, `"path" is required in a route configuration.`)
assert(
typeof route.component !== 'string',
`route config "component" for path: ${String(path || name)} cannot be a ` +
`string id. Use an actual component instead.`
)
}
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
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 },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
if (route.children) {
if (process.env.NODE_ENV !== 'production') {
if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
warn(
false,
`Named Route '${route.name}' has a default child route. ` +
`When navigating to this named route (:to="{name: '${route.name}'"), ` +
`the default child route will not be rendered. Remove the name from ` +
`this route and use the name of the default child route for named ` +
`links instead.`
)
}
}
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias)
? route.alias
: [route.alias]
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/'
)
})
}
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
`Duplicate named routes definition: ` +
`{ name: "${name}", path: "${record.path}" }`
)
}
}
}
- 我们只看几个关键逻辑,首先创建
RouteRecord
的代码如下:
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 }
}
- 这里要注意几个点,
path
是规范化后的路径,它会根据parent
的path
做计算;regex
是一个正则表达式的扩展,它利用了path-to-regexp
这个工具库,把path
解析成一个正则表达式的扩展,举个例子:
var keys = []
var re = pathToRegexp('/foo/:bar', keys)
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
components
是一个对象,通常我们在配置中写的component
实际上这里会被转换成{components: route.component}
;instances
表示组件的实例,也是一个对象类型;parent
表示父的RouteRecord
,因为我们配置的时候有时候会配置子路由,所以整个RouteRecord
也就是一个树型结构,如下所示:
if (route.children) {
// ...
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
- 如果配置了
children
,那么递归执行addRouteRecord
方法,并把当前的record
作为parent
传入,通过这样的深度遍历,我们就可以拿到一个route
下的完整记录,如下所示:
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
为
pathList
和pathMap
各添加一条记录。
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
// ...
}
-
如果我们在路由配置中配置了
name
,则给nameMap
添加一条记录。由于pathList
、pathMap
、nameMap
都是引用类型,所以在遍历整个routes
过程中去执行addRouteRecord
方法,会不断给他们添加数据。那么经过整个createRouteMap
方法的执行,我们得到的就是pathList
、pathMap
和nameMap
。其中pathList
是为了记录路由配置中的所有path
,而pathMap
和nameMap
都是为了通过path
和name
能快速查到对应的RouteRecord
。 -
在
createMatcher
函数,接下来就定义了一系列方法,最后返回了一个对象,如下所示:
return {
match,
addRoutes
}
也就是说,
matcher
是一个对象,它对外暴露了match
和addRoutes
方法。
addRoutes
,addRoutes
方法的作用是动态添加路由配置,因为在实际开发中有些场景是不能提前把路由写死的,需要根据一些条件动态添加路由,所以Vue-Router
也提供了这一接口:
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
addRoutes
的方法十分简单,再次调用createRouteMap
即可,传入新的routes
配置,由于pathList
、pathMap
、nameMap
都是引用类型,执行addRoutes
后会修改它们的值。
match
,如下所示:
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
const record = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
warn(record, `Route with name '${name}' does not exist`)
}
if (!record) return _createRoute(null, location)
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]
}
}
}
if (record) {
location.path = fillParams(record.path, location.params, `named route "${name}"`)
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]
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
return _createRoute(null, location)
}
match
方法接收三个参数,其中raw
是RawLocation
类型,它可以是一个url
字符串,也可以是一个Location
对象;currentRoute
是Route
类型,它表示当前的路径;redirectedFrom
和重定向相关,这里先忽略。match
方法返回的是一个路径,它的作用是根据传入的raw
和当前的路径currentRoute
计算出一个新的路径并返回。首先执行了normalizeLocation
,它的定义在src/util/location.js
中:
export function normalizeLocation (
raw: RawLocation,
current: ?Route,
append: ?boolean,
router: ?VueRouter
): Location {
let next: Location = typeof raw === 'string' ? { path: raw } : raw
if (next.name || next._normalized) {
return next
}
if (!next.path && next.params && current) {
next = assign({}, next)
next._normalized = true
const params: any = assign(assign({}, current.params), next.params)
if (current.name) {
next.name = current.name
next.params = params
} else if (current.matched.length) {
const rawPath = current.matched[current.matched.length - 1].path
next.path = fillParams(rawPath, params, `path ${current.path}`)
} else if (process.env.NODE_ENV !== 'production') {
warn(false, `relative params navigation requires a current route.`)
}
return next
}
const parsedPath = parsePath(next.path || '')
const basePath = (current && current.path) || '/'
const path = parsedPath.path
? resolvePath(parsedPath.path, basePath, append || next.append)
: basePath
const query = resolveQuery(
parsedPath.query,
next.query,
router && router.options.parseQuery
)
let hash = next.hash || parsedPath.hash
if (hash && hash.charAt(0) !== '#') {
hash = `#${hash}`
}
return {
_normalized: true,
path,
query,
hash
}
}
-
normalizeLocation
方法的作用是根据raw
,current
计算出新的location
,它主要处理了raw
的两种情况,一种是有params
且没有path
,一种是有path
的,对于第一种情况,如果current
有name
,则计算出的location
也有name
。 -
计算出新的
location
后,对location
的name
和path
的两种情况做了处理,如下所示:
-
name
,有name
的情况下就根据nameMap
匹配到record
,它就是一个RouterRecord
对象,如果record
不存在,则匹配失败,返回一个空路径;然后拿到record
对应的paramNames
,再对比currentRoute
中的params
,把交集部分的params
添加到location
中,然后在通过fillParams
方法根据record.path
和location.path
计算出location.path
,最后调用_createRoute(record, location, redirectedFrom)
去生成一条新路径。 -
path
,通过name
我们可以很快的找到record
,但是通过path
并不能,因为我们计算后的location.path
是一个真实路径,而record
中的path
可能会有param
,因此需要对所有的pathList
做顺序遍历, 然后通过matchRoute
方法根据record.regex
、location.path
、location.params
匹配,如果匹配到则也通过_createRoute(record, location, redirectedFrom)
去生成一条新路径。因为是顺序遍历,所以我们书写路由配置要注意路径的顺序,因为写在前面的会优先尝试匹配。
- 最后我们来看一下
_createRoute
的实现:
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
我们先不考虑
record.redirect
和record.matchAs
的情况,最终会调用createRoute
方法,它的定义在src/uitl/route.js
中:
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
query = clone(query)
} catch (e) {}
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route)
}
createRoute
可以根据record
和location
创建出来,最终返回的是一条Route
路径,我们之前也介绍过它的数据结构。在 Vue-Router 中,所有的Route
最终都会通过createRoute
函数创建,并且它最后是不可以被外部修改的。Route
对象中有一个非常重要属性是matched
,它通过formatMatch(record)
计算而来:
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
可以看它是通过
record
循环向上找parent
,直到找到最外层,并把所有的record
都 push 到一个数组中,最终返回的就是record
的数组,它记录了一条线路上的所有record
。matched
属性非常有用,它为之后渲染组件提供了依据。
- 总结:
matcher
相关的主流程的分析就结束了,我们了解了Location
、Route
、RouteRecord
等概念。并通过matcher
的match
方法,我们会找到匹配的路径Route
,这个对Route
的切换,组件的渲染都有非常重要的指导意义。
二、路径切换
history.transitionTo
是Vue-Router
中非常重要的方法,当我们切换路由线路的时候,就会执行到该方法,之前我们分析了matcher
的相关实现,知道它是如何找到匹配的新线路,那么匹配到新线路后又做了哪些事情,接下来我们来完整分析一下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()
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) })
}
})
}
transitionTo
首先根据目标location
和当前路径this.current
执行this.router.match
方法去匹配到目标的路径。这里this.current
是history
维护的当前路径,它的初始值是在history
的构造函数中初始化的:
this.current = START
START
的定义在src/util/route.js
中:
export const START = createRoute(null, {
path: '/'
})
- 这样就创建了一个初始的
Route
,而transitionTo
实际上也就是在切换this.current
,稍后我们会看到。拿到新的路径后,那么接下来就会执行confirmTransition
方法去做真正的切换,由于这个过程可能有一些异步的操作(如异步组件),所以整个confirmTransition
API
设计成带有成功回调函数和失败回调函数,先来看一下它的定义:
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
const abort = err => {
if (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) &&
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort()
}
const {
updated,
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
const queue: Array<?NavigationGuard> = [].concat(
extractLeaveGuards(deactivated),
this.router.beforeHooks,
extractUpdateHooks(updated),
activated.map(m => m.beforeEnter),
resolveAsyncComponents(activated)
)
this.pending = route
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' && (
typeof to.path === 'string' ||
typeof to.name === 'string'
))
) {
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
next(to)
}
})
} catch (e) {
abort(e)
}
}
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
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() })
})
}
})
})
}
- 首先定义了
abort
函数,然后判断如果满足计算后的route
和current
是相同路径的话,则直接调用this.ensureUrl
和abort
,ensureUrl
这个函数我们之后会介绍。接着又根据current.matched
和route.matched
执行了resolveQueue
方法解析出三个队列:
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)
}
}
-
因为
route.matched
是一个RouteRecord
的数组,由于路径是由current
变向route
,那么就遍历对比2
边的RouteRecord
,找到一个不一样的位置i
,那么next
中从0
到i
的RouteRecord
是两边都一样,则为updated
的部分;从i
到最后的RouteRecord
是next
独有的,为activated
的部分;而current
中从i
到最后的RouteRecord
则没有了,为deactivated
的部分。拿到updated
、activated
、deactivated
三个ReouteRecord
数组后,接下来就是路径变换后的一个重要部分,执行一系列的钩子函数。 -
导航守卫,官方的说法叫导航守卫,实际上就是发生在路由路径切换的时候,执行的一系列钩子函数。我们先从整体上看一下这些钩子函数执行的逻辑,首先构造一个队列
queue
,它实际上是一个数组;然后再定义一个迭代器函数iterator
;最后再执行runQueue
方法来执行这个队列。我们先来看一下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(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
- 这是一个非常经典的异步函数队列化执行的模式,
queue
是一个NavigationGuard
类型的数组,我们定义了step
函数,每次根据index
从queue
中取一个guard
,然后执行fn
函数,并且把guard
作为参数传入,第二个参数是一个函数,当这个函数执行的时候再递归执行step
函数,前进到下一个,注意这里的fn
就是我们刚才的iterator
函数,那么我们再回到iterator
函数的定义:
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' && (
typeof to.path === 'string' ||
typeof to.name === 'string'
))
) {
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
next(to)
}
})
} catch (e) {
abort(e)
}
}
iterator
函数逻辑很简单,它就是去执行每一个 导航守卫hook
,并传入route
、current
和匿名函数,这些参数对应文档中的to
、from
、next
,当执行了匿名函数,会根据一些条件执行abort
或next
,只有执行next
的时候,才会前进到下一个导航守卫钩子函数中,这也就是为什么官方文档会说只有执行next
方法来resolve
这个钩子函数。那么最后我们来看queue
是怎么构造的:
const queue: Array<?NavigationGuard> = [].concat(
extractLeaveGuards(deactivated),
this.router.beforeHooks,
extractUpdateHooks(updated),
activated.map(m => m.beforeEnter),
resolveAsyncComponents(activated)
)
- 按照顺序如下:
- 在失活的组件里调用离开守卫。
- 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 - 在激活的路由配置里调用
beforeEnter
。 - 解析异步路由组件。
-
接下来我们来分别介绍这 5 步的实现。
-
第一步是通过执行
extractLeaveGuards(deactivated)
,先来看一下extractLeaveGuards
的定义:
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
它内部调用了
extractGuards
的通用方法,可以从RouteRecord
数组中提取各个阶段的守卫:
function extractGuards (
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
const guards = flatMapComponents(records, (def, instance, match, 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)
}
这里用到了
flatMapComponents
方法去从records
中获取所有的导航,它的定义在src/util/resolve-components.js
中:
export function flatMapComponents (
matched: Array<RouteRecord>,
fn: Function
): Array<?Function> {
return flatten(matched.map(m => {
return Object.keys(m.components).map(key => fn(
m.components[key],
m.instances[key],
m, key
))
}))
}
export function flatten (arr: Array<any>): Array<any> {
return Array.prototype.concat.apply([], arr)
}
flatMapComponents
的作用就是返回一个数组,数组的元素是从matched
里获取到所有组件的key
,然后返回fn
函数执行的结果,flatten
作用是把二维数组拍平成一维数组。
那么对于
extractGuards
中flatMapComponents
的调用,执行每个fn
的时候,通过extractGuard(def, name)
获取到组件中对应name
的导航守卫:
function extractGuard (
def: Object | Function,
key: string
): NavigationGuard | Array<NavigationGuard> {
if (typeof def !== 'function') {
def = _Vue.extend(def)
}
return def.options[key]
}
获取到
guard
后,还会调用bind
方法把组件的实例instance
作为函数执行的上下文绑定到guard
上,bind
方法的对应的是bindGuard
:
function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
if (instance) {
return function boundRouteGuard () {
return guard.apply(instance, arguments)
}
}
}
那么对于
extractLeaveGuards(deactivated)
而言,获取到的就是所有失活组件中定义的beforeRouteLeave
钩子函数。
- 第二步是
this.router.beforeHooks
,在我们的VueRouter
类中定义了beforeEach
方法,在src/index.js
中:
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
function registerHook (list: Array<any>, fn: Function): Function {
list.push(fn)
return () => {
const i = list.indexOf(fn)
if (i > -1) list.splice(i, 1)
}
}
当用户使用
router.beforeEach
注册了一个全局守卫,就会往router.beforeHooks
添加一个钩子函数,这样this.router.beforeHooks
获取的就是用户注册的全局beforeEach
守卫。
- 第三步执行了
extractUpdateHooks(updated)
,来看一下extractUpdateHooks
的定义:
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}
和
extractLeaveGuards(deactivated)
类似,extractUpdateHooks(updated)
获取到的就是所有重用的组件中定义的beforeRouteUpdate
钩子函数。
-
第四步是执行
activated.map(m => m.beforeEnter)
,获取的是在激活的路由配置中定义的beforeEnter
函数。 -
第五步是执行
resolveAsyncComponents(activated)
解析异步组件,先来看一下resolveAsyncComponents
的定义,在src/util/resolve-components.js
中:
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
return (to, from, next) => {
let hasAsync = false
let pending = 0
let error = null
flatMapComponents(matched, (def, _, match, key) => {
if (typeof def === 'function' && def.cid === undefined) {
hasAsync = true
pending++
const resolve = once(resolvedDef => {
if (isESModule(resolvedDef)) {
resolvedDef = resolvedDef.default
}
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 {
const comp = res.component
if (comp && typeof comp.then === 'function') {
comp.then(resolve, reject)
}
}
}
}
})
if (!hasAsync) next()
}
}
-
resolveAsyncComponents
返回的是一个导航守卫函数,有标准的to
、from
、next
参数。它的内部实现很简单,利用了flatMapComponents
方法从matched
中获取到每个组件的定义,判断如果是异步组件,则执行异步组件加载逻辑,这块和我们之前分析Vue
加载异步组件很类似,加载成功后会执行match.components[key] = resolvedDef
把解析好的异步组件放到对应的components
上,并且执行next
函数。 -
这样在
resolveAsyncComponents(activated)
解析完所有激活的异步组件后,我们就可以拿到这一次所有激活的组件。这样我们在做完这五步后又做了一些事情:
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
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() })
})
}
})
})
-
在被激活的组件里调用
beforeRouteEnter
。 -
调用全局的
beforeResolve
守卫。 -
调用全局的
afterEach
钩子。
- 对于在被激活的组件里调用
beforeRouteEnter
的这些相关的逻辑:
const postEnterCbs = []
const isValid = () => this.current === route
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 bindEnterGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string,
cbs: Array<Function>,
isValid: () => boolean
): NavigationGuard {
return function routeEnterGuard (to, from, next) {
return guard(to, from, cb => {
next(cb)
if (typeof cb === 'function') {
cbs.push(() => {
poll(cb, match.instances, key, isValid)
})
}
})
}
}
function poll (
cb: any,
instances: Object,
key: string,
isValid: () => boolean
) {
if (instances[key]) {
cb(instances[key])
} else if (isValid()) {
setTimeout(() => {
poll(cb, instances, key, isValid)
}, 16)
}
}
extractEnterGuards
函数的实现也是利用了extractGuards
方法提取组件中的beforeRouteEnter
导航钩子函数,和之前不同的是bind
方法的不同。文档中特意强调了beforeRouteEnter
钩子函数中是拿不到组件实例的,因为当守卫执行前,组件实例还没被创建,但是我们可以通过传一个回调给next
来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
来看一下这是怎么实现的。
- 在
bindEnterGuard
函数中,返回的是routeEnterGuard
函数,所以在执行iterator
中的hook
函数的时候,就相当于执行routeEnterGuard
函数,那么就会执行我们定义的导航守卫guard
函数,并且当这个回调函数执行的时候,首先执行next
函数rersolve
当前导航钩子,然后把回调函数的参数,它也是一个回调函数用cbs
收集起来,其实就是收集到外面定义的postEnterCbs
中,然后在最后会执行:
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => { cb() })
})
}
-
在根路由组件重新渲染后,遍历
postEnterCbs
执行回调,每一个回调执行的时候,其实是执行poll(cb, match.instances, key, isValid)
方法,因为考虑到一些了路由组件被套transition
组件在一些缓动模式下不一定能拿到实例,所以用一个轮询方法不断去判断,直到能获取到组件实例,再去调用cb
,并把组件实例作为参数传入,这就是我们在回调函数中能拿到组件实例的原因。 -
第七步是获取
this.router.resolveHooks
,这个和
this.router.beforeHooks
的获取类似,在我们的VueRouter
类中定义了beforeResolve
方法:
beforeResolve (fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
当用户使用
router.beforeResolve
注册了一个全局守卫,就会往router.resolveHooks
添加一个钩子函数,这样this.router.resolveHooks
获取的就是用户注册的全局beforeResolve
守卫。
- 第八步是在最后执行了
onComplete(route)
后,会执行this.updateRoute(route)
方法:
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
同样在我们的
VueRouter
类中定义了afterEach
方法:
afterEach (fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
-
当用户使用
router.afterEach
注册了一个全局守卫,就会往router.afterHooks
添加一个钩子函数,这样this.router.afterHooks
获取的就是用户注册的全局afterHooks
守卫。所有导航守卫的执行分析完毕了,我们知道路由切换除了执行这些钩子函数,从表象上有两个地方会发生变化,一个是url
发生变化,一个是组件发生变化。接下来我们分别介绍这两块的实现原理。 -
url
,当我们点击router-link
的时候,实际上最终会执行router.push
,如下:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
this.history.push
函数,这个函数是子类实现的,不同模式下该函数的实现略有不同,我们来看一下平时使用比较多的hash
模式该函数的实现,在src/history/hash.js
中:
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)
}
push
函数会先执行this.transitionTo
做路径切换,在切换完成的回调函数中,执行pushHash
函数:
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
supportsPushState
的定义在src/util/push-state.js
中:
export const supportsPushState = inBrowser && (function () {
const ua = window.navigator.userAgent
if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
) {
return false
}
return window.history && 'pushState' in window.history
})()
如果支持的话,则获取当前完整的
url
,执行pushState
方法:
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
pushState
会调用浏览器原生的history
的pushState
接口或者replaceState
接口,更新浏览器的url
地址,并把当前url
压入历史栈中。然后在history
的初始化中,会设置一个监听器,监听历史栈的变化:
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)
}
})
})
}
- 当点击浏览器返回按钮的时候,如果已经有
url
被压入历史栈,则会触发popstate
事件,然后拿到当前要跳转的hash
,执行transtionTo
方法做一次路径转换。在使用Vue-Router
开发项目的时候,打开调试页面http://localhost:8080
后会自动把url
修改为http://localhost:8080/#/
,这是怎么做到呢?原来在实例化HashHistory
的时候,构造函数会执行ensureSlash()
方法:
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
export function getHash (): string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
const href = window.location.href
const index = href.indexOf('#')
return index === -1 ? '' : href.slice(index + 1)
}
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 replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
这个时候
path
为空,所以执行replaceHash('/' + path)
,然后内部会执行一次getUrl
,计算出来的新的url
为http://localhost:8080/#/
,最终会执行pushState(url, true)
,这就是url
会改变的原因。
- 组件,路由最终的渲染离不开组件,
Vue-Router
内置了<router-view>
组件,它的定义在src/components/view.js
中,如下所示:
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
data.routerView = true
const h = parent.$createElement
const name = props.name
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
if (inactive) {
return h(cache[name], data, children)
}
const matched = route.matched[depth]
if (!matched) {
cache[name] = null
return h()
}
const component = cache[name] = matched.components[name]
data.registerRouteInstance = (vm, val) => {
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
if (propsToPass) {
propsToPass = data.props = extend({}, propsToPass)
const attrs = data.attrs = data.attrs || {}
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key]
delete propsToPass[key]
}
}
}
return h(component, data, children)
}
}
<router-view>
是一个functional
组件,它的渲染也是依赖render
函数,那么<router-view>
具体应该渲染什么组件呢,首先获取当前的路径:
const route = parent.$route
我们之前分析过,在
src/install.js
中,我们给 Vue 的原型上定义了$route
:
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
然后在
VueRouter
的实例执行router.init
方法的时候,会执行如下逻辑,定义在src/index.js
中:
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
而
history.listen
方法定义在src/history/base.js
中:
listen (cb: Function) {
this.cb = cb
}
然后在
updateRoute
的时候执行this.cb
:
updateRoute (route: Route) {
//. ..
this.current = route
this.cb && this.cb(route)
// ...
}
- 我们执行
transitionTo
方法最后执行updateRoute
的时候会执行回调,然后会更新this.apps
保存的组件实例的_route
值,this.apps
数组保存的实例的特点都是在初始化的时候传入了router
配置项,一般的场景数组只会保存根Vue
实例,因为我们是在new Vue
传入了router
实例。$route
是定义在Vue.prototype
上。每个组件实例访问$route
属性,就是访问根实例的_route
,也就是当前的路由线路。<router-view>
是支持嵌套的,回到render
函数,其中定义了depth
的概念,它表示<router-view>
嵌套的深度。每个<router-view>
在渲染的时候,执行如下逻辑:
data.routerView = true
// ...
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
const matched = route.matched[depth]
// ...
const component = cache[name] = matched.components[name]
parent._routerRoot
表示的是根Vue
实例,那么这个循环就是从当前的<router-view>
的父节点向上找,一直找到根Vue
实例,在这个过程,如果碰到了父节点也是<router-view>
的时候,说明<router-view>
有嵌套的情况,depth++
。遍历完成后,根据当前线路匹配的路径和depth
找到对应的RouteRecord
,进而找到该渲染的组件。除了找到了应该渲染的组件,还定义了一个注册路由实例的方法:
data.registerRouteInstance = (vm, val) => {
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
- 给
vnode
的data
定义了registerRouteInstance
方法,在src/install.js
中,我们会调用该方法去注册路由的实例:
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 () {
// ...
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
- 在混入的
beforeCreate
钩子函数中,会执行registerInstance
方法,进而执行render
函数中定义的registerRouteInstance
方法,从而给matched.instances[name]
赋值当前组件的vm
实例。render
函数的最后根据component
渲染出对应的组件vonde
:
return h(component, data, children)
- 那么当我们执行
transitionTo
来更改路由线路后,组件是如何重新渲染的呢?在我们混入的beforeCreate
钩子函数中有这么一段逻辑:
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
// ...
}
})
-
由于我们把根
Vue
实例的_route
属性定义成响应式的,我们在每个<router-view>
执行render
函数的时候,都会访问parent.$route
,如我们之前分析会访问this._routerRoot._route
,触发了它的getter
,相当于<router-view>
对它有依赖,然后再执行完transitionTo
后,修改app._route
的时候,又触发了setter
,因此会通知<router-view>
的渲染watcher
更新,重新渲染组件。 -
Vue-Router
还内置了另一个组件<router-link>
,
它支持用户在具有路由功能的应用中(点击)导航。 通过to
属性指定目标地址,默认渲染成带有正确链接的<a>
标签,可以通过配置tag
属性生成别的标签。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的CSS
类名。<router-link>
比起写死的<a href="...">
会好一些,理由如下:
-
无论是
HTML5
history
模式还是hash
模式,它的表现行为一致,所以,当你要切换路由模式,或者在IE9
降级使用hash
模式,无须作任何变动。 -
在
HTML5
history
模式下,router-link
会守卫点击事件,让浏览器不再重新加载页面。 -
当你在
HTML5
history
模式下使用base
选项之后,所有的to
属性都不需要写(基路径)了。
- 那么接下来我们就来分析它的实现,它的定义在
src/components/link.js
中:
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click'
}
},
render (h: Function) {
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(this.to, current, this.append)
const classes = {}
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
const activeClassFallback = globalActiveClass == null
? 'router-link-active'
: globalActiveClass
const exactActiveClassFallback = globalExactActiveClass == null
? 'router-link-exact-active'
: globalExactActiveClass
const activeClass = this.activeClass == null
? activeClassFallback
: this.activeClass
const exactActiveClass = this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass
const compareTarget = location.path
? createRoute(null, location, null, router)
: route
classes[exactActiveClass] = isSameRoute(current, compareTarget)
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location)
} else {
router.push(location)
}
}
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => { on[e] = handler })
} else {
on[this.event] = handler
}
const data: any = {
class: classes
}
if (this.tag === 'a') {
data.on = on
data.attrs = { href }
} else {
const a = findAnchor(this.$slots.default)
if (a) {
a.isStatic = false
const extend = _Vue.util.extend
const aData = a.data = extend({}, a.data)
aData.on = on
const aAttrs = a.data.attrs = extend({}, a.data.attrs)
aAttrs.href = href
} else {
data.on = on
}
}
return h(this.tag, data, this.$slots.default)
}
}
<router-link>
标签的渲染也是基于render
函数,它首先做了路由解析:
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(this.to, current, this.append)
router.resolve
是VueRouter
的实例方法,它的定义在src/index.js
中:
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location,
route: Route,
href: string,
normalizedTo: Location,
resolved: Route
} {
const location = normalizeLocation(
to,
current || this.history.current,
append,
this
)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
normalizedTo: location,
resolved: route
}
}
function createHref (base: string, fullPath: string, mode) {
var path = mode === 'hash' ? '#' + fullPath : fullPath
return base ? cleanPath(base + '/' + path) : path
}
- 它先规范生成目标
location
,再根据location
和match
通过this.match
方法计算生成目标路径route
,然后再根据base
、fullPath
和this.mode
通过createHref
方法计算出最终跳转的href
。解析完router
获得目标location
、route
、href
后,接下来对exactActiveClass
和activeClass
做处理,当配置exact
为true
的时候,只有当目标路径和当前路径完全匹配的时候,会添加exactActiveClass
;而当目标路径包含当前路径的时候,会添加activeClass
。接着创建了一个守卫函数 :
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location)
} else {
router.push(location)
}
}
}
function guardEvent (e) {
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
if (e.defaultPrevented) return
if (e.button !== undefined && e.button !== 0) return
if (e.currentTarget && e.currentTarget.getAttribute) {
const target = e.currentTarget.getAttribute('target')
if (/\b_blank\b/i.test(target)) return
}
if (e.preventDefault) {
e.preventDefault()
}
return true
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => { on[e] = handler })
} else {
on[this.event] = handler
}
- 最终会监听点击事件或者其它可以通过
prop
传入的事件类型,执行hanlder
函数,最终执行router.push
或者router.replace
函数,它们的定义在src/index.js
中:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.replace(location, onComplete, onAbort)
}
实际上就是执行了
history
的push
和replace
方法做路由跳转。
最后判断当前
tag
是否是<a>
标签,<router-link>
默认会渲染成<a>
标签,当然我们也可以修改tag
的prop
渲染成其他节点,这种情况下会尝试找它子元素的<a>
标签,如果有则把事件绑定到<a>
标签上并添加href
属性,否则绑定到外层元素本身。
- 总结:路由的
transitionTo
的主体过程分析完毕了,其他一些分支比如重定向、别名、滚动行为等可以自行再去分析。路径变化是路由中最重要的功能,我们要记住以下内容:路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改url
,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据。