兩種路由模式的基本原理
用過 vue-router 就知道它提供了兩種模式鞋吉,hash
和 history
奄毡,通過 Router 構(gòu)建選項 mode 可進行配置折欠。
簡單理解 SPA 中的前端路由就是:
- 利用一些現(xiàn)有的 API 實現(xiàn) url 的改變,但不觸發(fā)瀏覽器主動加載新的 url吼过,新的頁面展示邏輯全部交給 js 控制锐秦;
- 給 history 中添加記錄,以實現(xiàn)頁面的后退前進盗忱;
前端路由下面通過兩個例子了解一下這兩種路由最基本的原理酱床。
hash 模式
hash 模式是通過修改 URL.hash (即 url 中 #
標(biāo)識符后的內(nèi)容)來實現(xiàn)的。
URL.hash 的改變不會觸發(fā)瀏覽器加載頁面趟佃,但會主動修改 history 記錄扇谣。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>router-hash</title>
</head>
<body>
// 頁面跳轉(zhuǎn)修改 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ù)
- 標(biāo)題 -- 暫無效 一般寫空字符串
- url -- 新的歷史 url 記錄
兩個方法的區(qū)別是 replaceState()
僅修改當(dāng)前記錄而非新建。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>router-history</title>
</head>
<body>
// 點擊后調(diào)用 go 函數(shù)跳轉(zhuǎn)路由
<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 了解一下整體的結(jié)構(gòu)和邏輯跋破。
以下代碼的 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);
// 記錄當(dāng)前展示的路由
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 對象定義一個響應(yīng)式 屬性
// 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 標(biāo)簽
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,梳理一下整體的結(jié)構(gòu):
- 首先是一個 VueRouter 類瓶蝴,并有一個 install 方法毒返,install 方法會在使用
Vue.use(VueRouter)
時被調(diào)用; - install 方法中添加了 Vue 原型對象上的兩個屬性
$router
$route
及router-view
router-link
兩個全局組件囊蓝; - VueRouter 類中通過構(gòu)造函數(shù)處理傳入的參數(shù)饿悬,生成路由映射表并調(diào)用 init 方法;
- init 方法中監(jiān)聽路由變化并改變 history.current聚霜;
- history.current 表示當(dāng)前路由狡恬,在 install 中被定義為了一個響應(yīng)式屬性
_route
,在該屬性被改變后會觸發(fā)依賴中的響應(yīng)已達到渲染router-view
中的組件蝎宇;
現(xiàn)在已經(jīng)對 VueRouter 有了一個最最基本的認識了弟劲,再去看源碼時就容易了一些。
淺嘗源碼
下面是我自己看 VueRouter 源碼并結(jié)合一些文章的學(xué)習(xí)筆記姥芥。
閱讀源碼的過程中寫了一些方便理解的注釋兔乞,希望給大家閱讀源碼帶來幫助,github: vue-router 源碼
vue-router 的 src 目錄如下凉唐,下面依次來分析這里主要的幾個文件的作用庸追。
index.js
VueRouter 的入口文件,主要作用是定義并導(dǎo)出了一個 VueRouter 類台囱。
下面是 index.js
源碼淡溯,刪除了 flow 相關(guān)的類型定義及函數(shù)的具體實現(xiàn),先來看一下整體的結(jié)構(gòu)和每部分的功能簿训。
// ...
// 導(dǎo)出 VueRouter 類
export default class VueRouter {
// 定義類的靜態(tài)屬性及方法
// install 用于 vue 的插件機制咱娶,Vue.use 時會自動調(diào)用 install 方法
static install: () => void;
static version: string;
// 構(gòu)造函數(shù) 用于處理實例化時傳入的參數(shù)
constructor (options) {}
// 獲取到路由路徑對應(yīng)的組件實例
match ( raw, current, redirectedFrom ) {}
// 返回 history.current 當(dāng)前路由路徑
get currentRoute () {}
// 存入根組件實例,并監(jiān)聽路由的改變
init (app) {}
// 注冊一些全局鉤子函數(shù)
beforeEach (fn) {} // 全局前置守衛(wèi)
beforeResolve (fn) {} // 全局解析守衛(wèi)
afterEach (fn) {} // 全局后置鉤子
onReady (cb, errorCb) {} // 路由完成初始導(dǎo)航時調(diào)用
onError (errorCb) {} // 路由導(dǎo)航過程中出錯時被調(diào)用
// 注冊一些 history 導(dǎo)航函數(shù)
push (location, onComplete, onAbort) {}
replace (location, onComplete, onAbort) {}
go (n) {}
back () {}
forward () {}
// 獲取路由對應(yīng)的組件
getMatchedComponents (to) {}
// 解析路由表
resolve (to, current, append) {}
// 添加路由表 并自動跳轉(zhuǎn)到首頁
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 存在則自動調(diào)用 Vue.use 注冊該路由插件
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
下面看一些主要方法的具體實現(xiàn)膘侮。
constructor
VueRouter 構(gòu)造函數(shù),主要做了 3 件事:
- 初始化傳入的參數(shù) options 的榛,即調(diào)用
new VueRouter()
時傳入的參數(shù)琼了; - 創(chuàng)建 match 匹配函數(shù),用于匹配當(dāng)前 path 或 name 對應(yīng)的路由組件困曙;
- 根據(jù)不同模式生成 history 實例表伦,history 實例提供一些跳轉(zhuǎn)谦去、監(jiān)聽等方法慷丽;
關(guā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ù)里調(diào)用,傳入根組件實例锄俄。
// 傳入根組件實例
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)
// 設(shè)置 app 銷毀程序
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// 當(dāng)銷毀時局劲,將 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
// 跳轉(zhuǎn)到當(dāng)前路由
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
)
}
// 設(shè)置路由監(jiān)聽奶赠,路由改變時改變 _route 屬性鱼填,表示當(dāng)前路由
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
install.js
該文件主要是定義并導(dǎo)出一個 install 方法,在 Vue.use(VueRouter)
時被調(diào)用毅戈。
install 方法主要做了這幾件事:
- 通過全局混入 beforeCreate 鉤子函數(shù)的方式苹丸,為每個 vue 組件實例添加了指向同一個路由實例的
_routerRoot
屬性,使得每個組件中都可以獲取到路由信息及方法苇经。 - Vue.prototype 上掛載
$router
$route
兩個屬性赘理,分別表示路由實例及當(dāng)前路由晃酒。 - 全局注冊 router-link router-view 組件啤咽。
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
// 若已調(diào)用過則直接返回
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)) {
// 設(shè)置根路由
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
文件導(dǎo)出一個 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 查詢到對應(yīng)的路由記錄對象扮碧,路由記錄對象包含 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),
// 若非命名視圖組件,則設(shè)為默認視圖組件
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 導(dǎo)航路由跳轉(zhuǎn)則默認子路由將不會渲染
// 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 邏輯 增加對應(yīng)的 記錄
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褂始,具體可以看一下對應(yīng)的 issue
簡單的說就是當(dāng)命名路由有默認子路由時
routes: [{
path: '/home',
name: 'home',
component: Home,
children: [{
path: '',
name: 'home.index',
component: HomeIndex
}]
}]
使用 to="/home"
會跳轉(zhuǎn)到 HomeIndex 默認子路由,而使用 :to="{ name: 'home' }"
則只會跳轉(zhuǎn)到 Home 并不會顯示HomeIndex 默認子路由描函。
通過上面 addRouteRecord 函數(shù)源碼就能知道這兩種跳轉(zhuǎn)方式 path 和 name 表現(xiàn)不同的原因了:
因為通過 path 和 name 是分別從兩個映射表查找對應(yīng)路由記錄的崎苗,
pathMap 生成過程中是先遞歸子路由,如上例舀寓,當(dāng)添加該子路由的路由記錄時胆数,key 就是 /home
,子路由添加完后父路由添加時判斷 /home
已存在則不會添加進 pathMap互墓。
而 nameMap 的 key 是 name必尼,home
對應(yīng)的就是 Home 組件,home.index
對應(yīng) HomeIndex篡撵。
create-matcher.js
createMatcher 函數(shù)根據(jù)路由配置調(diào)用 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 類,主要的作用是:
路由變化時通過調(diào)用 transitionTo 方法以獲取到對應(yīng)的路由記錄并依次執(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, () => {
// 以下為切換路由成功或失敗的回調(diào)
// 更新路由信息,對組件的 _route 屬性進行賦值宿刮,觸發(fā)組件渲染
// 調(diào)用 afterHooks 中的鉤子函數(shù)
this.updateRoute(route)
// 添加 hashchange 監(jiān)聽
onComplete && onComplete(route)
// 更新 URL
this.ensureURL()
// 只執(zhí)行一次 ready 回調(diào)
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
// 中斷跳轉(zhuǎn)路由函數(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)
}
// 如果是相同的路由就不跳轉(zhuǎn)
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()
}
// 通過對比路由解析出可復(fù)用的組件互站,需要渲染的組件,失活的組件
const { updated, deactivated, activated } = resolveQueue(this.current.matched, route.matched)
// 導(dǎo)航守衛(wèi)數(shù)組
const queue = [].concat(
// 失活的組件鉤子
extractLeaveGuards(deactivated),
// 全局 beforeEach 鉤子
this.router.beforeHooks,
// 在當(dāng)前路由改變僵缺,但是該組件被復(fù)用時調(diào)用
extractUpdateHooks(updated),
// 需要渲染組件 enter 守衛(wèi)鉤子
activated.map(m => m.beforeEnter),
// 解析異步路由組件
resolveAsyncComponents(activated)
)
// 保存路由
this.pending = route
// 迭代器胡桃,用于執(zhí)行 queue 中的導(dǎo)航守衛(wèi)鉤子
const iterator = (hook, next) => {
// 路由不相等就不跳轉(zhuǎn)路由
if (this.pending !== route) {
return abort()
}
try {
// 執(zhí)行鉤子
hook(route, current, (to) => {
// 只有執(zhí)行了鉤子函數(shù)中的 next,才會繼續(xù)執(zhí)行下一個鉤子函數(shù)
// 否則會暫停跳轉(zhuǎn)
// 以下邏輯是在判斷 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
// 當(dāng)所有異步組件加載完成后磕潮,會執(zhí)行這里的回調(diào)翠胰,也就是 runQueue 中的 cb()
// 接下來執(zhí)行 需要渲染組件的導(dǎo)航守衛(wèi)鉤子
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
// 隊列中的函數(shù)都執(zhí)行完畢,就執(zhí)行回調(diào)函數(shù)
runQueue(queue, iterator, () => {
// 跳轉(zhuǎn)完成
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)
})
}
}