前言
相信用vue開發過專案的小夥伴都不會陌生,vue-router模組幫助我們處理單頁面應用的理由跳轉的,我們只需要將不同path對應的元件資訊傳給vue-router,就可以在頁面區域性重新整理的情況下實現路由跳轉了,你有沒有覺得對這一處理過程感到很好奇,想要揭開這一操作的神祕面紗?來吧,讓我們一起開啟探索之旅~
vue-router的使用
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'history',
routes: [...]
})
new Vue({
router
...
})
複製程式碼
我們看到在使用路由之前需要呼叫Vue.use(VueRouter),這一操作就好像給一個拼裝玩具的安裝核心部件的過程.這使得VueRouter可以使用Vue.下面簡單介紹下Vue.use方法.
export function initUse(Vue: GlobalAPI) {
Vue.use = function(plugin) {
// 獲取當前外掛列表
const installedPlugins = this._installedPlugins || (this._installedPlugins = []) if(this.installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
// 插入vue
args.unshift(this)
// 一般外掛都有一個install函式
// 通過該函式可以讓外掛使用Vue
if(typeof plugin.install === 'function') {
plugin.install.apply(plugin, args) // this指向plugin
} else {
plugin.apply(null, args) // this指向window
}
}
}
複製程式碼
整體概覽
vue-router整體結構思維導圖
深入原始碼
接下來我會和你一起一步步深入原始碼,探究整個路由切換的實現邏輯.
目錄結構
拿到一個專案的原始碼的時候,我們首先要去觀察他的檔案結構,對專案的整體結構有一個大致的瞭解.主要的原始碼邏輯都在src目錄下:
|—— index.js
|—— install.js
└── create-matcher.js
└── create-route-map.js
|—— components
|—— link.js
|—— view.js
|—— history
|—— abstract.js
|—— base.js
|—— errors.js
|—— hash.js
|—— html5.js
└── util
|—— async.js
|—— dom.js
|—— location.js
|—— misc.js
|—— params.js
|—— path.js
|—— push-state.js
|—— query.js
|—— resolve-components.js
|—— route.js
|—— scroll.js
|—— warn.js
複製程式碼
- index.js整個專案的入口檔案,用來定義VueRouter這個類
- install.js定義vue-router外掛的掛載邏輯
- create-matcher.js是我們建立matcher的入口檔案
- create-route-map.js用於建立路由對映表的js, pathMap,nameMap等
- components資料夾存放的是vue-router的兩個元件,RouterView和RouterLink
- history資料夾,存放三種路由模式的處理邏輯,hash模式,history模式,abstract模式以及路由跳轉錯誤的處理邏輯
- util資料夾,顧名思義存放工具函式的地方
入口檔案
// ./index.js
import { install } from './install
import { START } from './util/route'
import { HashHistory } from './history/hash'
import { HTML5History } from './history/html5'
import { AbstractHistory } from './history/abstract'
import type { Matcher } from './create-matcher'
// ...
// VueRouter類
export default class VueRouter {
constructor (options: RouterOptions = {}) {
// 根例項
this.app = null
// 元件例項陣列
this.apps = []
// vue-router的鉤子函式
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 建立路由匹配例項,傳入我們定義的routes
this.matcher = createMatcher(options.routes || [], this)
// 判斷模式
let mode = options.mode || 'hash'
// fallback不等於false,且mode傳入history但是不支援pushState api的時候調整路由模式為hash
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
// 非瀏覽器環境mode='abstract'
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 根據不同的模式建立對應的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}`)
}
}
}
// vue-router 初始化函式
init (app: any /* Vue component instance */) {
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)
// 建立一個元件銷燬的處理程式
// https://github.com/vuejs/vue-router/issues/2639
app.$once('hook:destroyed', () => {
// 如果元件陣列中存在某個對應元件的例項,則清除
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// 確保我們有一個根元件或者null如果沒有元件的情況下
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
})
// 根元件如果已經建立,則直接返回,我們不需要在建立一個新的history 監聽
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
)
}
// 呼叫history物件的listen方法,主要是為了把回撥中觸發元件的_route物件的監聽的邏輯// cb函式賦值給history物件的cb屬性,以便在路由更新的時候呼叫
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
/*
下面是
vue-router的一系列api
...
*/
VueRouter.install = install // 掛載install函式
VueRouter.version = '__VERSION__' // 定義版本號
// 判斷如果window上掛載了Vue則自動使用外掛
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter)
}
}
複製程式碼
模組安裝--install函式
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
// 獲取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, c allVal)
}
}
// 給Vue例項的鉤子函式混入一些屬性,並新增_route響應式物件
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
// 將根元件的_routerRoot屬相指向Vue例項
this._routerRoot = this
// 將根元件_router屬性指向傳入的router物件
this._router = this.$options.router
// router初始化,呼叫vueRouter的init方法
this._router.init(this)
// 呼叫Vue的defineReactive增加_route的響應式物件
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 將每個元件的_routerRoot屬性都指向根Vue例項
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 註冊vueComponent進行Observer處理
registerInstance(this, this)
},
destroyed () {
// 登出VueComponent
registerInstance(this)
}
})
// 給Vue例項新增 $router屬性,指向 _router 為VueRouter的例項
// _route為一個存數量路有資料的物件
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 註冊元件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// vue鉤子合併策略
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
複製程式碼
這一操作主要是:
-
讓VueRouter可以使用Vue,
-
給Vue例項的beforeCreate鉤子函式中混入以下邏輯:
(1)加上_routerRoot, _router 屬性
(2)初始化vueRouter
(3) 新增_router的響應式物件,這樣Vue例項可以監聽到路由的變化
-
全域性註冊兩個路由元件, RouterView和RouterLink
建立Matcher
還記得入口檔案裡有這樣一步操作嗎?
this.matcher = createMatcher(options.routes || [], this)
複製程式碼
下面讓我們一起來看看createMatcher函式.
// ./create-matcher.js
/*
建立matcher
@params { Array } routes 初始化的時候傳進來的路由配置
@params { Object } router vueRouter的例項
*/
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// 建立路由對映表, pathList,路由path組成的陣列,
// pathMap路由path和routeRecord組成的對映表
// nameMap路由name和routeRecord組成的對映表
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) {
// 當前路由對應的name存在則在nameMap中查詢對應的record
const record = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
warn(record, `Route with name '${name}' does not exist`)
}
// 如果沒找到對應的record則建立對應的record
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 = {}
}
// 複製路由的引數到location中
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]
}
}
}
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)
}
}
}
// no match
return _createRoute(null, location)
}
// 處理重定向
function redirect (
record: RouteRecord,
location: Location
): Route {
// ...
}
// 處理路由的別名,併為其對應的建立路由
function alias (
record: RouteRecord,
location: Location,
matchAs: string
): Route {
// ...
}
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
}
}
function matchRoute (
regex: RouteRegExp,
path: string,
params: Object
): boolean {
const m = path.match(regex)
if (!m) {
return false
} else if (!params) {
return true
}
for (let i = 1, len = m.length; i < len; ++i) {
const key = regex.keys[i - 1]
const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i]
if (key) {
// Fix #1994: using * with props: true generates a param named 0
params[key.name || 'pathMatch'] = val
}
}
return true
}
// ...
複製程式碼
匹配路由之前會為每個路由建立對應的路由對映表
// ./create-route-map.js
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// pathList被建立用來控制path匹配優先順序
const pathList: Array<string> = oldPathList || []
// $flow-disable-line
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
routes.forEach(route => {
// 為每個路由對映一個組裝好的record物件
addRouteRecord(pathList, pathMap, nameMap, route)
})
// ensure wildcard routes are always at the end
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
}
}
// 為每個路由對映一個record物件
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
// 從每個路由配置物件中解構出path和name
const { path, name } = route
if (process.env.NODE_ENV !== 'production') {
// ...
}
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, // path
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) {
// Warn if route is named, does not redirect and has a default child route.
// If users navigate to this route by name, the default child will
// not be rendered (GH Issue #629)
if (process.env.NODE_ENV !== 'production') {
if (
route.name &&
!route.redirect &&
route.children.some(child => /^\/?$/.test(child.path))
) {
// ...
}
}
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
if (!pathMap[record.path]) {
pathList.push(record.path)
// 建立path到record的對映
pathMap[record.path] = record
}
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
for (let i = 0; i < aliases.length; ++i) {
const alias = aliases[i]
if (process.env.NODE_ENV !== 'production' && alias === path) {
// ...
}
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
}
}
if (name) {
if (!nameMap[name]) {
// 建立name到record的對映
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
// ..
}
}
}
複製程式碼
這個檔案主要建立了三個變數:
- pathList: 用來存放處理過的所有路由的path, 被建立用來控制path匹配的優先順序
- pathMap: 路由的path與record物件的對映
- nameMap: 路由的name與record物件的對映 record物件存放了完整的路由資訊,可以通過path或者name來對映出路由的完整資訊,來進行相關的路有操作.
三種路由模式
vur-router為我們提供了三種路由模式,對應的就是history資料夾裡存放的內容,base.js建立了history的基類,hash.js,html5.js和abstract.js則在基類的基礎上,擴充套件自身模式對應的屬性和方法.
- hash模式
- history模式
- abstract模式
abstract模式是Node.js中使用的路由模式,主要原理是用陣列來模擬瀏覽器記錄,然後通過對陣列的操作來模擬路由的前進後退.這裡我們主要介紹瀏覽器中使用的兩種路由模式,hash模式和history模式.
base.js
export class History {
router: Router // vueRouter類
base: string // 基礎路徑
current: Route // 當前路由
pending: ?Route
cb: (r: Route) => void // 路由切換的回撥
ready: boolean //
readyCbs: Array<Function>
readyErrorCbs: Array<Function>
errorCbs: Array<Function>
// implemented by sub-classes
+go: (n: number) => void
+push: (loc: RawLocation) => void
+replace: (loc: RawLocation) => void
+ensureURL: (push?: boolean) => void
+getCurrentLocation: () => string
constructor (router: Router, base: ?string) {
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: Function) {
this.cb = cb
}
// 路由切換
onReady (cb: Function, errorCb: ?Function) {
// 路由切換完成則執行回撥,否則push進readyCbs陣列中
if (this.ready) {
cb()
} else {
this.readyCbs.push(cb)
if (errorCb) {
this.readyErrorCbs.push(errorCb)
}
}
}
// 把發生錯誤時需要執行的回撥收集起來
onError (errorCb: Function) {
this.errorCbs.push(errorCb)
}
// 核心函式,控制路由跳轉
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()
// 保證readyCbs陣列中的回撥函式製備呼叫一次
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)
})
}
}
)
}
/*
* @mathods 判斷是否跳轉
* @params { Route } route 匹配的路由物件
* @params { Function } onComplete 跳轉完成時的回撥
* @params { Function } onAbort 取消跳轉時的回撥
*/
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
const abort = err => {
// after merging https://github.com/vuejs/vue-router/pull/2771 we
// When the user navigates through history through back/forward buttons
// we do not want to throw the error. We only throw it if directly calling
// push/replace. That's why it's not included in isError
// 當使用者通過瀏覽器操作前進後退按鈕的時候,我們不想丟擲錯誤,
// 我們僅僅會在直接呼叫push和replace方法的時候丟擲錯誤
if (!isExtendedError(NavigationDuplicated, err) && 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(new NavigationDuplicated(route))
}
// 下面是跳轉的邏輯
// 通過對比解析出可複用的元件,失活的元件, 需要渲染的元件,
// matched裡存放的是路由記錄的陣列
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
// 切換路由要做的一系列任務佇列
const queue: Array<?NavigationGuard> = [].concat(
// 清除失活的元件, 通過觸發beforeRouteLeave導航鉤子,執行清除對應元件的路由記錄等邏輯
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// 可複用的元件, 通過觸發 beforeRouteUpdate 導航鉤子,來做一些更新邏輯
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// 解析要啟用的非同步元件, 也有對應的 beforeRouteEnter導航鉤子
resolveAsyncComponents(activated)
)
this.pending = route
// 用迭代器類執行queue中的導航守衛鉤子
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
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('/') or next({ path: '/' }) -> redirect
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
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()
})
})
}
})
})
}
// 更新路由
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
}
複製程式碼
history物件的基礎類主要處理了路由跳轉的邏輯,在路由跳轉過程中,先獲取路由的匹配資訊,未找到匹配的路由資訊則建立新的路由,然後判斷是否跳轉.跳轉則比較跳轉前後的路由資訊,解析出失活的元件,可複用的元件和需要啟用的元件,並呼叫對應路由導航鉤子函式,從而更新路由資訊.
hash.js 和 html5.js
// hash.js
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
// 這是一個延遲,知道app掛載完成,以免hashChange的監聽被過早的觸發
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
// 開啟路由切換的監聽,如果支援pushState api,
// 則監聽popState事件,不支援,則監聽hashChange事件
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)
}
})
}
)
}
// 跳轉到新的路由
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
)
}
// 替換當前路由
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
// 前進或後退到某個路由
go (n: number) {
window.history.go(n)
}
// 更新url
ensureURL (push?: boolean) {
const current = this.current.fullPath
if (getHash() !== current) {
push ? pushHash(current) : replaceHash(current)
}
}
getCurrentLocation () {
return getHash()
}
}
複製程式碼
html5.js的內容和hash.js的內容大同小異,不同的就是hash.js中會去判斷瀏覽器是否支援pushState api,支援的話監聽popState事件,不支援的話監聽hashChange事件,而html5.js是直接監聽popState事件.
base基類擴充套件的HashHistory類和HTML5History類,主要增加了以下內容:
-
監聽路由切換,保證在app掛載之後開啟監聽,index.js中相關程式碼
// 否則建立根元件 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 ) } 複製程式碼
-
為history物件擴充套件push.replace,go,ensureURL,getCurrentLocation等方法.
全域性路由元件
RouterView
export default {
// 元件名稱
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
// used by devtools to display a router-view badge
data.routerView = true
// directly use parent context's createElement() function
// so that components rendered by router-view can resolve named slots
// 父元素的createElement方法
const h = parent.$createElement
const name = props.name
// 獲取當前路由物件
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
// determine current view depth, also check to see if the tree
// has been toggled inactive but kept-alive.
// 獲取元件層級,知道 _routerRoot 指向Vue例項時終止迴圈
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
const vnodeData = parent.$vnode && parent.$vnode.data
if (vnodeData) {
if (vnodeData.routerView) {
depth++
}
if (vnodeData.keepAlive && parent._inactive) {
inactive = true
}
}
parent = parent.$parent
}
data.routerViewDepth = depth
// render previous view if the tree is inactive and kept-alive
// 如果元件被快取,則渲染快取的元件
if (inactive) {
return h(cache[name], data, children)
}
// 根據元件層級去查詢route路由物件中匹配的元件
const matched = route.matched[depth]
// 如果沒找到匹配的元件,則渲染空節點
if (!matched) {
cache[name] = null
return h()
}
// 將查詢出來的元件也賦值給cache[name]
const component = cache[name] = matched.components[name]
// attach instance registration hook
// this will be called in the instance's injected lifecycle hooks
// 新增註冊鉤子, 鉤子會被注入到元件的生命週期鉤子中
// 這會在install.js中給Vue中元件的生命週期混入鉤子中呼叫
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
// 給prepatch的鉤子函式也註冊該例項, 為了同一個元件可以在不同的路由下複用
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
// 給初始化的鉤子函式中也註冊該例項,以便路由發生變哈的時候啟用快取的元件
data.hook.init = (vnode) => {
if (vnode.data.keepAlive &&
vnode.componentInstance &&
vnode.componentInstance !== matched.instances[name]
) {
matched.instances[name] = vnode.componentInstance
}
}
// 處理props
let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
if (propsToPass) {
// clone to prevent mutation
propsToPass = data.props = extend({}, propsToPass)
// pass non-declared props as attrs
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]
}
}
}
// 對應當前route物件的元件存在, 且沒有在快取, 執行渲染操作
return h(component, data, children)
}
}
複製程式碼
RouterLink
// ./components/link.js
export default {
// 元件名稱
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a' // 預設建立a標籤
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click'
}
},
render (h: Function) {
// 獲取掛載的VueRouter例項
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
// Support global empty active class
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 = route.redirectedFrom
? createRoute(null, normalizeLocation(route.redirectedFrom), 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 }
const scopedSlot =
!this.$scopedSlots.$hasNormal &&
this.$scopedSlots.default &&
this.$scopedSlots.default({
href,
route,
navigate: handler,
isActive: classes[activeClass],
isExactActive: classes[exactActiveClass]
})
if (scopedSlot) {
if (scopedSlot.length === 1) {
return scopedSlot[0]
} else if (scopedSlot.length > 1 || !scopedSlot.length) {
if (process.env.NODE_ENV !== 'production') {
warn(
false,
`RouterLink with to="${
this.props.to
}" is trying to use a scoped slot but it didn't provide exactly one child.`
)
}
return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
}
}
if (this.tag === 'a') {
data.on = on
data.attrs = { href }
} else {
// 從子元素中找到第一個a標籤,給他繫結事件監聽和設定href屬性
// find the first <a> child and apply listener and href
const a = findAnchor(this.$slots.default)
if (a) {
// in case the <a> is a static node
a.isStatic = false
const aData = (a.data = extend({}, a.data))
aData.on = on
const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
aAttrs.href = href
} else {
// doesn't have <a> child, apply listener to self
// 沒有找到a標籤,則繫結當前元素自身
data.on = on
}
}
return h(this.tag, data, this.$slots.default)
}
}
複製程式碼
總結
vue-router的原始碼解讀到此就告一段落了,我的github倉庫有完整的程式碼註解和部分模組的思維導圖倉庫地址,大家如果對此有興趣,想要學習的同學一定先把vue-router git倉庫的程式碼克隆下來,對照我的解讀來看,或者可以先自己試著去讀,當然直白的讀原始碼會顯得有些枯燥,你可以嘗試帶著自己的問題或者疑惑去讀,有目的性的閱讀更容易堅持.不明白的地方可以多讀幾遍,原始碼中函式的邏輯往往用到了不同js檔案中的函式,要順著思路去往下捋.我沒有對util檔案裡用到的一些鉤子函式做過多的解讀,但是希望大家都可以去仔細研究下,可以收穫到更多的設計思路和程式設計技巧.剛入原始碼坑不久,如果有不對的或者解釋不到位的地方歡迎指出,有什麼建議或想法,歡迎留言或者加微信lj_de_wei_xin
與我交流~