一、 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
,同樣也會渲染對應的組件,切換完畢後會把目標線路更新替換當前線路,這樣就會作爲下一次的路徑切換的依據。