
兩種路由模式的基本原理
用過 vue-router 就知道它提供了兩種模式,hash 和 history ,通過 Router 構建選項 mode 可進行配置。
簡單理解 SPA 中的前端路由就是:
- 利用一些現(xiàn)有的 API 實現(xiàn) url 的改變,但不觸發(fā)瀏覽器主動加載新的 url,新的頁面展示邏輯全部交給 js 控制;
- 給 history 中添加記錄,以實現(xiàn)頁面的后退前進;
前端路由下面通過兩個例子了解一下這兩種路由最基本的原理。
hash 模式
hash 模式是通過修改 URL.hash (即 url 中 # 標識符后的內(nèi)容)來實現(xiàn)的。
URL.hash 的改變不會觸發(fā)瀏覽器加載頁面,但會主動修改 history 記錄。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>router-hash</title>
</head>
<body>
// 頁面跳轉修改 hash
<a href="#/home">Home</a>
<a href="#/about">About</a>
<div id="app"></div>
</body>
<script>
// 頁面加載完后根據(jù) hash 顯示頁面內(nèi)容
window.addEventListener('load', () => {
app.innerHTML = location.hash.slice(1)
})
// 監(jiān)聽 hash 改變后修改頁面顯示內(nèi)容
window.addEventListener('hashchange', () => {
app.innerHTML = location.hash.slice(1)
})
</script>
</html>
history 模式
history 模式 主要原理是使用了瀏覽器的 history API,主要是 history.pushState() 和 history.replaceState() 兩個方法。
通過這兩種方法修改 history 的 url 記錄時,瀏覽器不會檢查并加載新的 url 。
這兩個方法都是接受三個參數(shù):
- 狀態(tài)對象 -- 可以用來暫存一些數(shù)據(jù)
- 標題 -- 暫無效 一般寫空字符串
- url -- 新的歷史 url 記錄
兩個方法的區(qū)別是 replaceState() 僅修改當前記錄而非新建。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>router-history</title>
</head>
<body>
// 點擊后調用 go 函數(shù)跳轉路由
<a onclick="go('/home')">Home</a>
<a onclick="go('/about')">About</a>
<div id="app"></div>
</body>
<script>
// 修改 history 記錄及頁面內(nèi)容
function go(pathname) {
history.pushState(null, '', pathname)
app.innerHTML = pathname
}
// 監(jiān)聽瀏覽器的前進后退并修改頁面內(nèi)容
window.addEventListener('popstate', () => {
app.innerHTML = location.pathname
})
</script>
</html>
手寫一個超簡易的 VueRouter
看源碼之前,先通過一個簡易的 VueRouter 了解一下整體的結構和邏輯。
以下代碼的 git 地址:simple-vue-router
class HistoryRoute {
constructor() {
this.current = null
}
}
class VueRouter {
constructor(opts) {
this.mode = opts.mode || 'hash';
this.routes = opts.routes || [];
// 創(chuàng)建路由映射表
this.routesMap = this.creatMap(this.routes);
// 記錄當前展示的路由
this.history = new HistoryRoute();
this.init();
}
// 初始化 動態(tài)修改 history.current
init() {
if (this.mode === 'hash') {
location.hash ? '' : location.hash = '/';
window.addEventListener('load', () => {
this.history.current = location.hash.slice(1);
});
window.addEventListener('hashchange', () => {
this.history.current = location.hash.slice(1);
});
} else {
location.pathname ? '' : location.pathname = '/';
window.addEventListener('load', () => {
this.history.current = location.pathname;
});
window.addEventListener('popstate', () => {
this.history.current = location.pathname;
})
}
}
// 創(chuàng)建路由映射表
// {
// '/': HomeComponent,
// '/about': AboutCompontent
// }
creatMap(routes) {
return routes.reduce((memo, current) => {
memo[current.path] = current.component;
return memo;
}, {})
}
}
// Vue.use(Router) 時觸發(fā)
VueRouter.install = function (Vue) {
// 定義 $router $route 屬性
Object.defineProperty(Vue.prototype, '$router', {
get() {
return this.$root._router;
}
});
Object.defineProperty(Vue.prototype, '$route', {
get() {
return this.$root._route;
}
});
// 全局混入 beforeCreate 鉤子函數(shù)
Vue.mixin({
beforeCreate() {
// 通過 this.$options.router 判斷為根實例
if (this.$options && this.$options.router) {
this._router = this.$options.router;
// 給 this 對象定義一個響應式 屬性
// https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js
Vue.util.defineReactive(this, '_route', this._router.history);
}
},
});
// 渲染函數(shù) & JSX https://cn.vuejs.org/v2/guide/render-function.html
// 注冊全局組件 router-link
// 默認渲染為 a 標簽
Vue.component('router-link', {
props: {
to: String,
tag: String
},
methods: {
handleClick() {
const mode = this._self.$root._router.mode;
location.href = mode === 'hash' ? `#${this.to}` : this.to;
}
},
render: function (h) {
const mode = this._self.$root._router.mode;
const tag = this.tag || 'a';
return (
<tag
on-click={ tag !== 'a' && this.handleClick }
href={ mode === 'hash' ? `#${this.to}` : this.to }
>
{ this.$slots.default }
</tag>
);
}
});
// 注冊全局組件 router-view
// 根據(jù) history.current 從 路由映射表中獲取到對象組件并渲染
Vue.component('router-view', {
render: function (h) {
const current = this._self.$root._route.current;
const routeMap = this._self.$root._router.routesMap;
return h(routeMap[current]);
}
});
}
export default VueRouter;
120 行代碼實現(xiàn)了最最基本的 VueRouter,梳理一下整體的結構:
- 首先是一個 VueRouter 類,并有一個 install 方法,install 方法會在使用
Vue.use(VueRouter)時被調用; - install 方法中添加了 Vue 原型對象上的兩個屬性
$router$route及router-viewrouter-link兩個全局組件; - VueRouter 類中通過構造函數(shù)處理傳入的參數(shù),生成路由映射表并調用 init 方法;
- init 方法中監(jiān)聽路由變化并改變 history.current;
- history.current 表示當前路由,在 install 中被定義為了一個響應式屬性
_route,在該屬性被改變后會觸發(fā)依賴中的響應已達到渲染router-view中的組件;
現(xiàn)在已經(jīng)對 VueRouter 有了一個最最基本的認識了,再去看源碼時就容易了一些。
淺嘗源碼
下面是我自己看 VueRouter 源碼并結合一些文章的學習筆記。
閱讀源碼的過程中寫了一些方便理解的注釋,希望給大家閱讀源碼帶來幫助,github: vue-router 源碼
vue-router 的 src 目錄如下,下面依次來分析這里主要的幾個文件的作用。

index.js
VueRouter 的入口文件,主要作用是定義并導出了一個 VueRouter 類。
下面是 index.js 源碼,刪除了 flow 相關的類型定義及函數(shù)的具體實現(xiàn),先來看一下整體的結構和每部分的功能。
// ...
// 導出 VueRouter 類
export default class VueRouter {
// 定義類的靜態(tài)屬性及方法
// install 用于 vue 的插件機制,Vue.use 時會自動調用 install 方法
static install: () => void;
static version: string;
// 構造函數(shù) 用于處理實例化時傳入的參數(shù)
constructor (options) {}
// 獲取到路由路徑對應的組件實例
match ( raw, current, redirectedFrom ) {}
// 返回 history.current 當前路由路徑
get currentRoute () {}
// 存入根組件實例,并監(jiān)聽路由的改變
init (app) {}
// 注冊一些全局鉤子函數(shù)
beforeEach (fn) {} // 全局前置守衛(wèi)
beforeResolve (fn) {} // 全局解析守衛(wèi)
afterEach (fn) {} // 全局后置鉤子
onReady (cb, errorCb) {} // 路由完成初始導航時調用
onError (errorCb) {} // 路由導航過程中出錯時被調用
// 注冊一些 history 導航函數(shù)
push (location, onComplete, onAbort) {}
replace (location, onComplete, onAbort) {}
go (n) {}
back () {}
forward () {}
// 獲取路由對應的組件
getMatchedComponents (to) {}
// 解析路由表
resolve (to, current, append) {}
// 添加路由表 并自動跳轉到首頁
addRoutes (routes) {}
}
// 注冊鉤子函數(shù),push 存入數(shù)組
function registerHook (list, fn) {}
// 根據(jù)模式(hash / history)拼接 location.href
function createHref (base, fullPath, mode) {}
// 掛載靜態(tài)屬性及方法
VueRouter.install = install
VueRouter.version = '__VERSION__'
// 瀏覽器環(huán)境下且 window.Vue 存在則自動調用 Vue.use 注冊該路由插件
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
下面看一些主要方法的具體實現(xiàn)。
constructor
VueRouter 構造函數(shù),主要做了 3 件事:
- 初始化傳入的參數(shù) options ,即調用
new VueRouter()時傳入的參數(shù); - 創(chuàng)建 match 匹配函數(shù),用于匹配當前 path 或 name 對應的路由組件;
- 根據(jù)不同模式生成 history 實例,history 實例提供一些跳轉、監(jiān)聽等方法;
關于 history 實例 及 match 匹配函數(shù)后面會講到。
constructor(options = {}) {
this.app = null // 根組件實例,在 init 中獲取并賦值
this.apps = [] // 保存多個根組件實例,在 init 中被添加
this.options = options // 傳入配置項參數(shù)
this.beforeHooks = [] // 初始化全局前置守衛(wèi)
this.resolveHooks = [] // 初始化全局解析守衛(wèi)
this.afterHooks = [] // 初始化全局后置鉤子
this.matcher = createMatcher(options.routes || [], this) // 創(chuàng)建 match 匹配函數(shù)
let mode = options.mode || 'hash' // 默認 hash 模式
// history 瀏覽器環(huán)境不支持時向下兼容使用 hash 模式
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
// 非瀏覽器環(huán)境強制使用 abstract 模式
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 根據(jù)不同模式生成 history 實例
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}`)
}
}
}
init
install.js 中,init 函數(shù)會在根組件實例的 beforeCreate 生命周期函數(shù)里調用,傳入根組件實例。
// 傳入根組件實例
init(app) {
// 非生產(chǎn)環(huán)境進行未安裝路由的斷言報錯提示
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
// 保存該根組件實例
this.apps.push(app)
// 設置 app 銷毀程序
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// 當銷毀時,將 app 從 this.apps 數(shù)組中清除,防止內(nèi)存溢出
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
})
// app 已初始化則直接返回
if (this.app) {
return
}
this.app = app
// 跳轉到當前路由
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 設置路由監(jiān)聽,路由改變時改變 _route 屬性,表示當前路由
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
install.js
該文件主要是定義并導出一個 install 方法,在 Vue.use(VueRouter) 時被調用。
install 方法主要做了這幾件事:
- 通過全局混入 beforeCreate 鉤子函數(shù)的方式,為每個 vue 組件實例添加了指向同一個路由實例的
_routerRoot屬性,使得每個組件中都可以獲取到路由信息及方法。 - Vue.prototype 上掛載
$router$route兩個屬性,分別表示路由實例及當前路由。 - 全局注冊 router-link router-view 組件。
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
// 若已調用過則直接返回
if (install.installed && _Vue === Vue) return
install.installed = true
// install 函數(shù)中將 Vue 賦值給 _Vue
// 可在其他模塊中不用引入直接使用 Vue 對象
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
// 每個組件混入 beforeCreate 鉤子函數(shù)的實現(xiàn)
Vue.mixin({
beforeCreate () {
// 判斷是否存在 router 對象,若存在則為根實例
if (isDef(this.$options.router)) {
// 設置根路由
this._routerRoot = this
this._router = this.$options.router
// 路由初始化,將根實例傳入 VueRouter 的 init 方法
this._router.init(this)
// _router 屬性雙向綁定
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 非根實例則通過 $parent 指向父級的 _routerRoot 屬性,最終指向根實例
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// 注入 $router $route
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 全局注冊 router-link router-view組件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
create-route-map.js
create-route-map.js 文件導出一個 createRouteMap 方法,用于創(chuàng)建路由根據(jù) path 和 name 的映射表。
// ...
// 創(chuàng)建路由 map
export function createRouteMap(routes, oldPathList, oldPathMap, oldNameMap) {
// pathList 用于控制路徑匹配優(yōu)先級
const pathList = oldPathList || []
// 根據(jù) path 的路由映射表
const pathMap = oldPathMap || Object.create(null)
// 根據(jù) name 的路由映射表
const nameMap = oldNameMap || Object.create(null)
// 遍歷路由配置添加到 pathList pathMap nameMap 中
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
}
}
function addRouteRecord(pathList, pathMap, nameMap, route, parent, matchAs) {}
function compileRouteRegex(path, pathToRegexpOptions) {}
function normalizePath(path, parent, strict) {}
addRouteRecord
createRouteMap 函數(shù)中最重要的一步就是 遍歷路由配置并添加到映射表中 的 addRouteRecord 函數(shù)。
addRouteRecord 函數(shù)作用是生成兩個映射表,PathMap 和 NameMap,分別可以通過 path 和 name 查詢到對應的路由記錄對象,路由記錄對象包含 meta、props、及最重要的 components 視圖組件實例用于渲染在 router-view 組件中。
// 增加 路由記錄 函數(shù)
function addRouteRecord(pathList, pathMap, nameMap, route, parent, matchAs) {
// 獲取 path, name
const { path, name } = route
// 編譯正則的選項
const pathToRegexpOptions = route.pathToRegexpOptions || {}
// 格式化 path
// 根據(jù) pathToRegexpOptions.strict 判斷是否刪除末尾斜杠 /
// 根據(jù)是否以斜杠 / 開頭判斷是否需要拼接父級路由的路徑
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict // 末尾斜杠是否精確匹配 (default: false)
)
// 匹配規(guī)則是否大小寫敏感?(默認值:false)
// 路由配置中 caseSensitive 和 pathToRegexpOptions.sensitive 作用相同
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
// 路由記錄 對象
const record = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
// 若非命名視圖組件,則設為默認視圖組件
components: route.components || {
default: route.component
},
instances: {},
name,
parent,
matchAs, // alias 匹配的路由記錄 path 為別名,需根據(jù) matchAs 匹配
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null ? {} : route.components ?
route.props : {
default: route.props
}
}
if (route.children) {
// 如果是命名路由,沒有重定向,并且有默認子路由,則發(fā)出警告。
// 如果用戶通過 name 導航路由跳轉則默認子路由將不會渲染
// https://github.com/vuejs/vue-router/issues/629
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.`
)
}
}
// 遞歸路由配置的 children 屬性,添加路由記錄
route.children.forEach(child => {
// 別名匹配時真正的 path 為 matchAs
const childMatchAs = matchAs ?
cleanPath(`${matchAs}/${child.path}`) :
undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 處理別名 alias 邏輯 增加對應的 記錄
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 || '/' // matchAs
)
})
}
// 更新 path map
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}" }`
)
}
}
}
遞歸子路由中還有一個警告,對于命名路由且有默認子路由時在開發(fā)環(huán)境給出提示。
這個提示用于避免一個 bug,具體可以看一下對應的 issue
簡單的說就是當命名路由有默認子路由時
routes: [{
path: '/home',
name: 'home',
component: Home,
children: [{
path: '',
name: 'home.index',
component: HomeIndex
}]
}]
使用 to="/home" 會跳轉到 HomeIndex 默認子路由,而使用 :to="{ name: 'home' }" 則只會跳轉到 Home 并不會顯示HomeIndex 默認子路由。
通過上面 addRouteRecord 函數(shù)源碼就能知道這兩種跳轉方式 path 和 name 表現(xiàn)不同的原因了:
因為通過 path 和 name 是分別從兩個映射表查找對應路由記錄的,
pathMap 生成過程中是先遞歸子路由,如上例,當添加該子路由的路由記錄時,key 就是 /home ,子路由添加完后父路由添加時判斷 /home 已存在則不會添加進 pathMap。
而 nameMap 的 key 是 name,home 對應的就是 Home 組件,home.index 對應 HomeIndex。
create-matcher.js
createMatcher 函數(shù)根據(jù)路由配置調用 createRouteMap 方法建立映射表,并提供了 匹配路由記錄 match 及 添加路由記錄 addRoutes 兩個方法。
addRoutes 用于動態(tài)添加路由配置;
match 用于根據(jù)傳入的 location 和 路由對象 返回一個新的路由對象;
// 參數(shù) routes 表示創(chuàng)建 VueRouter 傳入的 routes 配置信息
// router 表示 VueRouter 實例
export function createMatcher(routes, router) {
// 創(chuàng)建路由映射表
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes(routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
// 路由匹配
function match(raw, currentRoute, redirectedFrom) {
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
// 命名路由處理
// 合并 location 及 record 的數(shù)據(jù)并返回一個新的路由對象
} else if (location.path) {
// 普通路由處理
// 合并 location 及 record 的數(shù)據(jù)并返回一個新的路由對象
}
// 沒有匹配到路由記錄則返回一個空的路由對象
return _createRoute(null, location)
}
function redirect(record, location) {
// ...
}
function alias(record, location, matchAs) {
// ...
}
// 根據(jù)條件創(chuàng)建不同的路由
function _createRoute(record, location, redirectedFrom) {
// 處理 重定向 redirect
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
// 處理 別名 alias
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoutes
}
}
history/base.js
history/base.js 中定義了一個 History 類,主要的作用是:
路由變化時通過調用 transitionTo 方法以獲取到對應的路由記錄并依次執(zhí)行一系列守衛(wèi)鉤子函數(shù);
export class History {
constructor (router, base) {
this.router = router
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
}
listen (cb) {
this.cb = cb
}
onReady (cb, errorCb) {
if (this.ready) {
cb()
} else {
this.readyCbs.push(cb)
if (errorCb) {
this.readyErrorCbs.push(errorCb)
}
}
}
onError (errorCb) {
this.errorCbs.push(errorCb)
}
// 切換路由,在 VueRouter 初始化及監(jiān)聽路由改變時會觸發(fā)
transitionTo (location, onComplete, onAbort) {
// 獲取匹配的路由信息
const route = this.router.match(location, this.current)
// 確認切換路由
this.confirmTransition(route, () => {
// 以下為切換路由成功或失敗的回調
// 更新路由信息,對組件的 _route 屬性進行賦值,觸發(fā)組件渲染
// 調用 afterHooks 中的鉤子函數(shù)
this.updateRoute(route)
// 添加 hashchange 監(jiān)聽
onComplete && onComplete(route)
// 更新 URL
this.ensureURL()
// 只執(zhí)行一次 ready 回調
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) })
}
})
}
// 確認切換路由
confirmTransition (route, onComplete, onAbort) {
const current = this.current
// 中斷跳轉路由函數(shù)
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) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort()
}
// 通過對比路由解析出可復用的組件,需要渲染的組件,失活的組件
const { updated, deactivated, activated } = resolveQueue(this.current.matched, route.matched)
// 導航守衛(wèi)數(shù)組
const queue = [].concat(
// 失活的組件鉤子
extractLeaveGuards(deactivated),
// 全局 beforeEach 鉤子
this.router.beforeHooks,
// 在當前路由改變,但是該組件被復用時調用
extractUpdateHooks(updated),
// 需要渲染組件 enter 守衛(wèi)鉤子
activated.map(m => m.beforeEnter),
// 解析異步路由組件
resolveAsyncComponents(activated)
)
// 保存路由
this.pending = route
// 迭代器,用于執(zhí)行 queue 中的導航守衛(wèi)鉤子
const iterator = (hook, next) => {
// 路由不相等就不跳轉路由
if (this.pending !== route) {
return abort()
}
try {
// 執(zhí)行鉤子
hook(route, current, (to) => {
// 只有執(zhí)行了鉤子函數(shù)中的 next,才會繼續(xù)執(zhí)行下一個鉤子函數(shù)
// 否則會暫停跳轉
// 以下邏輯是在判斷 next() 中的傳參
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' && (
typeof to.path === 'string' ||
typeof to.name === 'string'
))
) {
// next('/') 或者 next({ path: '/' }) -> 重定向
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// 這里執(zhí)行 next
// 也就是執(zhí)行下面函數(shù) runQueue 中的 step(index + 1)
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}
// 同步執(zhí)行異步函數(shù)
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
// 當所有異步組件加載完成后,會執(zhí)行這里的回調,也就是 runQueue 中的 cb()
// 接下來執(zhí)行 需要渲染組件的導航守衛(wèi)鉤子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
// 隊列中的函數(shù)都執(zhí)行完畢,就執(zhí)行回調函數(shù)
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() })
})
}
})
})
}
updateRoute (route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
}