funfish, 玩弄內心的鬼, Vue.js 技術揭祕的文章,對個人幫助html
vue-router的源碼不算不少, 可是內容也不算少。本文談不上逐行分析, 可是會盡可能詳盡的說明主流程和原理。對一些工具函數和邊緣條件的處理會略過,由於我也沒有逐行去了解它們,請見諒。vue
咱們在學習VueRouter源碼前,先來複習下hash以及histroy相關的知識。更多細節請參考mdn文檔,本節內容節選自mdn文檔。html5
當URL的片斷標識符更改時,將觸發hashchange事件 (跟在#符號後面的URL部分,包括#符號)。注意 histroy.pushState() 絕對不會觸發 hashchange 事件,即便新的URL與舊的URL僅哈希不一樣也是如此。node
pushState()須要三個參數: 一個狀態對象, 一個標題(目前被忽略), 和一個URL。react
history.replaceState()的使用與history.pushState()很是類似,區別在於replaceState()是修改了當前的歷史記錄項而不是新建一個。webpack
調用history.pushState()或者history.replaceState()不會觸發popstate事件. popstate事件只會在瀏覽器某些行爲下觸發, 好比點擊後退、前進按鈕(或者在JavaScript中調用history.back()、history.forward()、history.go()方法)。git
若是當前處於激活狀態的歷史記錄條目是由history.pushState()方法建立, 或者由history.replaceState()方法修改過的, 則popstate事件對象的state屬性包含了這個歷史記錄條目的state對象的一個拷貝。github
一般構建一個Vue應用的時候, 咱們會使用Vue.use以插件的形式安裝VueRouter。同時會在Vue的實例上掛載router的實例。web
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
let a = new Vue({
router,
render: h => h(App)
}).$mount('#app')
複製代碼
import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/about',
name: 'about',
component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
}
]
})
複製代碼
在Vue的文檔中指出Vue.js 的插件應該有一個公開方法 install。這個方法的第一個參數是 Vue 構造器,第二個參數是一個可選的選項對象, 咱們首先查看源碼中install.js的文件。vue-router
在install文件中, 咱們在Vue的實例上初始化了一些私有屬性
在Vue的prototype上初始化了一些getter
而且在全局混入了mixin, 已經全局註冊了RouterView, RouterLink組件.
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
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)
}
}
Vue.mixin({
beforeCreate () {
// 判斷是否實例是否掛載了router
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
// _router, 劫持的是當前的路由
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
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)
const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
複製代碼
Vue.util.defineReactive, 這是Vue裏面觀察者劫持數據的方法,劫持_route,當_route觸發setter方法的時候,則會通知到依賴的組件。而RouterView, 須要訪問parent.$route因此造成了依賴(咱們在後面會看到)
👀咱們到Vue中看一下defineReactive的源碼, 在defineReactive, 會對_route使用Object.defineProperty劫持setter方法。set時會通知觀察者。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ...
},
set: function reactiveSetter (newVal) {
// ...
childOb = !shallow && observe(newVal)
dep.notify()
}
})
複製代碼
export default class VueRouter {
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
// fallback會在不支持history環境的狀況下, 回退到hash模式
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
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}`)
}
}
}
}
複製代碼
matcher對象中包含了兩個屬性, addRoutes, match。
pathList, pathMap, nameMap分別是路徑的列表, 路徑和路由對象的映射, 路由名稱和路由對象的映射。vue-router目標支持動態路由, pathList, pathMap, nameMap能夠在初始化後動態的被修改。它們由createRouteMap方法建立, 咱們來看看createRouteMap的源碼。
export function createRouteMap ( routes, oldPathList, oldPathMap, oldNameMap ) {
// pathList,pathMap,nameMap支持後續的動態添加
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)
})
// 將通配符的路徑, push到pathList的末尾
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
}
}
複製代碼
routes爲一組路由, 因此咱們循環routes, 可是route可能存在children因此咱們經過遞歸的形式建立route。返回一個route的樹🌲
function addRouteRecord ( pathList, pathMap, nameMap, route, parent, matchAs ) {
const { path, name } = route
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
// normalizePath, 會對path進行格式化
// 會刪除末尾的/,若是route是子級,會鏈接父級和子級的path,造成一個完整的path
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 }
}
// 若是route存在children, 咱們會遞歸的建立路由對象
// 遞歸的建立route對象
if (route.children) {
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 || '/' // matchAs
)
})
}
// 填充pathMap,nameMap,pathList
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
}
}
複製代碼
動態添加更多的路由規則, 並動態的修改pathList,pathMap,nameMap
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
複製代碼
match方法根據參數raw(能夠是字符串也能夠Location對象), 以及currentRoute(當前的路由對象返回Route對象),在nameMap中查找對應的Route,並返回。
若是location包含name, 我經過nameMap找到了對應的Route, 可是此時path中可能包含params, 因此咱們會經過fillParams函數將params填充到patch,返回一個真實的路徑path。
function match ( raw, currentRoute, redirectedFrom ) {
// 會對raw,currentRoute處理,返回格式化後path, hash, 以及params
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
const record = nameMap[name]
if (!record) return _createRoute(null, location)
// 獲取全部必須的params。若是optional爲true說明params不是必須的
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) {
// 使用params對path進行填充返回一個真實的路徑
location.path = fillParams(record.path, location.params, `named route "${name}"`)
// 建立Route對象
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]
// 使用pathList中的每個regex,對path進行匹配
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
return _createRoute(null, location)
}
複製代碼
咱們接下來繼續看看_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)
}
複製代碼
其中redirect,alias最終都會調用createRoute方法。咱們再將視角轉向createRoute函數。createRoute函數會返回一個凍結的Router對象。
其中matched屬性爲一個數組,包含當前路由的全部嵌套路徑片斷的路由記錄。數組的順序爲從外向裏(樹的外層到內層)。
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)
}
複製代碼
init中。會掛載cb的回調,這關乎到RouteView的渲染。咱們根據當前的url,在Vue根實例的beforeCreate生命週期鉤子中完成路由的初始化,完成第一次的路由導航。
init (app) {
// app爲Vue的實例
this.apps.push(app)
if (this.app) {
return
}
// 在VueRouter上掛載app屬性
this.app = app
const history = this.history
// 初始化當前的路由,完成第一次導航,在hash模式下會在transitionTo的回調中調用setupListeners
// setupListeners裏會對hashchange事件進行監聽
// transitionTo是進行路由導航的函數,咱們將會在下面介紹
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
// 掛載了回調的cb, 每次更新路由更好更新_route
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
複製代碼
history一共有三個模式hash, histroy, abstract, 這三個類都繼承至base類
咱們首先看下base的構造函數, 其中router是VueRouter的實例, base是路由的基礎路徑。current是當前的路由默認爲"/", ready是路由的狀態, readyCbs是ready的回調的集合, readyErrorCbs是raday失敗的回調。errorCbs導航出錯的回調的集合。
export class History {
constructor (router: Router, base: ?string) {
this.router = router
// normalizeBase會對base路徑作出格式化的處理,會爲base開頭自動添加‘/’,刪除結尾的‘/’,默認返回’/‘
this.base = normalizeBase(base)
// 初始化的當前路由對象
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
}
}
複製代碼
export const START = createRoute(null, {
path: '/'
})
複製代碼
function normalizeBase (base: ?string): string {
if (!base) {
// inBrowser判斷是否爲瀏覽器環境
if (inBrowser) {
const baseEl = document.querySelector('base')
base = (baseEl && baseEl.getAttribute('href')) || '/'
base = base.replace(/^https?:\/\/[^\/]+/, '')
} else {
base = '/'
}
}
if (base.charAt(0) !== '/') {
base = '/' + base
}
return base.replace(/\/$/, '')
}
複製代碼
base中的listen的方法,會在VueRouter的init方法中使用到,listen會給每一次的路由的更新,添加回調
listen (cb: Function) {
this.cb = cb
}
複製代碼
base類中還有一些其餘方法好比,transitionTo,confirmTransition,updateRoute它們在base子類中被使用。咱們立刻在hashrouter中再看看它們的具體實現。
在HashHistory的構造函數中。咱們會判斷當前的fallback是否爲true。若是爲true,使用checkFallback,添加’#‘,並使用window.location.replace替換文檔。
若是fallback爲false,咱們會調用ensureSlash,ensureSlash會爲沒有「#」的url,添加「#」,而且使用histroy的API或者replace替換文檔。
因此咱們在訪問127.0.0.1的時候,會自動替換爲127.0.0.1/#/
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// 若是是回退hash的狀況,而且判斷當前路徑是否有/#/。若是沒有將會添加'/#/'
if (fallback && checkFallback(this.base)) {
return
}
ensureSlash()
}
}
複製代碼
checkFallback
// 檢查url是否包含‘/#/’
function checkFallback (base) {
// 獲取hash值
const location = getLocation(base)
// 若是location不是以/#,開頭。添加/#,使用window.location.replace替換文檔
if (!/^\/#/.test(location)) {
window.location.replace(
cleanPath(base + '/#' + location)
)
return true
}
}
複製代碼
// 返回hash
export function getLocation (base) {
let path = decodeURI(window.location.pathname)
if (base && path.indexOf(base) === 0) {
path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
}
複製代碼
// 刪除 //, 替換爲 /
export function cleanPath (path) {
return path.replace(/\/\//g, '/')
}
複製代碼
ensureSlash
function ensureSlash (): boolean {
// 判斷是否包含#,並獲取hash值。若是url沒有#,則返回‘’
const path = getHash()
// 判斷path是否以/開頭
if (path.charAt(0) === '/') {
return true
}
// 若是開頭不是‘/’, 則添加/
replaceHash('/' + path)
return false
}
複製代碼
// 獲取「#」後面的hash
export function getHash (): string {
const href = window.location.href
const index = href.indexOf('#')
return index === -1 ? '' : decodeURI(href.slice(index + 1))
}
複製代碼
function replaceHash (path) {
// supportsPushState判斷是否存在history的API
// 使用replaceState或者window.location.replace替換文檔
// getUrl獲取完整的url
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
複製代碼
// getUrl返回了完整了路徑,而且會添加#, 確保存在/#/
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
複製代碼
在replaceHash中,咱們調用了replaceState方法,在replaceState方法中,又調用了pushState方法。在pushState中咱們會調用saveScrollPosition方法,它會記錄當前的滾動的位置信息。而後使用histroyAPI,或者window.location.replace完成文檔的更新。
export function replaceState (url?: string) {
pushState(url, true)
}
export function pushState (url?: string, replace?: boolean) {
// 記錄當前的x軸和y軸,以發生導航的時間爲key,位置信息記錄在positionStore中
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)
}
}
複製代碼
咱們把push,replace放在一塊兒說,由於它們實現的源碼都是相似的。在push和replace中,調用transitionTo方法,transitionTo方法在基類base中,咱們如今轉過頭來看看transitionTo的源碼(👇往下兩節,代碼不是很難,可是callback嵌套callback, 如蜜傳如蜜,看起來仍是比較噁心的)
push (location, onComplete, onAbort) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
replace (location, onComplete, onAbort) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
複製代碼
transitionTo的location參數是咱們的目標路徑, 能夠是string或者RawLocation對象。咱們經過router.match方法(咱們在在matcher介紹過),router.match會返回咱們的目標路由對象。緊接着咱們會調用confirmTransition函數。
transitionTo (location, onComplete, onAbort) {
const route = this.router.match(location, this.current)
this.confirmTransition(
route,
() => {
// ...
},
err => {
// ...
}
)
}
複製代碼
confirmTransition函數中會使用,isSameRoute會檢測是否導航到相同的路由,若是導航到相同的路由會中止🤚導航,並執行終止導航的回調。
if (
isSameRoute(route, current) &&
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort()
}
複製代碼
接着咱們調用resolveQueue方法,resolveQueue接受當前的路由和目標的路由的matched屬性做爲參數,resolveQueue的工做方式能夠以下圖所示。咱們會逐一比較兩個數組的路由,尋找出須要銷燬的,須要更新的,須要激活的路由,並返回它們(由於咱們須要執行它們不一樣的路由守衛)
function resolveQueue ( current next ) {
let i
// 依次比對當前的路由和目標的路由的matched屬性中的每個路由
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)
}
}
複製代碼
下一步,咱們會逐一提取出,全部要執行的路由守衛,將它們concat到隊列queue。queue裏存放裏全部須要在此次路由更新中執行的路由守衛。
第一步,咱們使用extractLeaveGuards函數,提取出deactivated中全部須要銷燬的組件內的「beforeRouteLeave」的守衛。extractLeaveGuards函數中會調用extractGuards函數,extractGuards函數,會調用flatMapComponents函數,flatMapComponents函數會遍歷records(resolveQueue返回deactivated), 在遍歷過程當中咱們將組件,組件的實例,route對象,傳入了fn(extractGuards中傳入flatMapComponents的回調), 在fn中咱們會獲取組件中beforeRouteLeave守衛。
// 返回每個組件中導航的集合
function extractLeaveGuards (deactivated) {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
function extractGuards ( records, name, bind, reverse? ) {
const guards = flatMapComponents(
records,
// def爲組件
// instance爲組件的實例
(def, instance, match, key) => {
// 返回每個組件中定義的路由守衛
const guard = extractGuard(def, name)
if (guard) {
// bindGuard函數確保了guard(路由守衛)的this指向的是Component中的實例
return Array.isArray(guard)
? guard.map(guard => bind(guard, instance, match, key))
: bind(guard, instance, match, key)
}
}
)
// 返回導航的集合
return flatten(reverse ? guards.reverse() : guards)
}
export function flatMapComponents ( matched, fn ) {
// 遍歷matched,並返回matched中每個route中的每個Component
return flatten(matched.map(m => {
// 若是沒有設置components則默認是components{ default: YouComponent },能夠從addRouteRecord函數中看到
// 將每個matched中全部的component傳入fn中
// m.components[key]爲components中的key鍵對應的組件
// m.instances[key]爲組件的實例,這個屬性是在routerview組件中beforecreated中被賦值的
return Object.keys(m.components).map(key => fn(
m.components[key],
m.instances[key],
m,
key
))
}))
}
// 返回一個新數組
export function flatten (arr) {
return Array.prototype.concat.apply([], arr)
}
// 獲取組件中的屬性
function extractGuard (def, key) {
if (typeof def !== 'function') {
def = _Vue.extend(def)
}
return def.options[key]
}
// 修正函數的this指向
function bindGuard (guard, instance) {
if (instance) {
return function boundRouteGuard () {
return guard.apply(instance, arguments)
}
}
}
複製代碼
第二步,獲取全局VueRouter對象beforeEach的守衛
第三步, 使用extractUpdateHooks函數,提取出update組件中全部的beforeRouteUpdate的守衛。過程同第一步相似。
第四步, 獲取activated的options配置中beforeEach守衛
第五部, 獲取全部的異步組件
在獲取全部的路由守衛後咱們定義了一個迭代器iterator。接着咱們使用runQueue遍歷queue隊列。將queue隊列中每個元素傳入fn(迭代器iterator)中,在迭代器中會執行路由守衛,而且路由守衛中必須明確的調用next方法纔會進入下一個管道,進入下一次迭代。迭代完成後,會執行runQueue的callback。
在runQueue的callback中,咱們獲取激活組件內的beforeRouteEnter的守衛,而且將beforeRouteEnter守衛中next的回調存入postEnterCbs中,在導航被確認後遍歷postEnterCbs執行next的回調。
在queue隊列執行完成後,confirmTransition函數會執行transitionTo傳入的onComplete的回調。往下看👇
// queue爲路由守衛的隊列
// fn爲定義的迭代器
export function runQueue (queue, fn, cb) {
const step = index => {
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
// 使用迭代器處理每個鉤子
// fn是迭代器
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
// 迭代器
const iterator = (hook, next) => {
if (this.pending !== route) {
return abort()
}
try {
// 傳入路由守衛三個參數,分別分別對應to,from,next
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
// 若是next的參數爲false
this.ensureURL(true)
abort(to)
} else if (
// 若是next須要重定向到其餘路由
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
// 獲取全部激活組件內部的路由守衛beforeRouteEnter,組件內的beforeRouteEnter守衛,是沒法獲取this實例的
// 由於這時激活的組件尚未建立,可是咱們能夠經過傳一個回調給next來訪問組件實例。
// beforeRouteEnter (to, from, next) {
// next(vm => {
// // 經過 `vm` 訪問組件實例
// })
// }
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
// 獲取全局的beforeResolve的路由守衛
const queue = enterGuards.concat(this.router.resolveHooks)
// 再一次遍歷queue
runQueue(queue, iterator, () => {
// 完成過渡
if (this.pending !== route) {
return abort()
}
// 正在過渡的路由設置爲null
this.pending = null
//
onComplete(route)
// 導航被確認後,咱們執行beforeRouteEnter守衛中,next的回調
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => { cb() })
})
}
}
)
})
// 獲取組件中的beforeRouteEnter守衛
function extractEnterGuards ( activated, cbs, isValid ) {
return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
// 這裏沒有修改guard(守衛)中this的指向
return bindEnterGuard(guard, match, key, cbs, isValid)
})
}
// 將beforeRouteEnter守衛中next的回調push到postEnterCbs中
function bindEnterGuard ( guard, match, key, cbs, isValid ) {
// 這裏的next參數是迭代器中傳入的參數
return function routeEnterGuard (to, from, next) {
return guard(to, from, cb => {
// 執行迭代器中傳入的next,進入下一個管道
next(cb)
if (typeof cb === 'function') {
// 咱們將next的回調包裝後保存到cbs中,next的回調會在導航被確認的時候執行回調
cbs.push(() => {
poll(cb, match.instances, key, isValid)
})
}
})
}
}
複製代碼
在confirmTransition的onComplete回調中,咱們調用updateRoute方法, 參數是導航的路由。在updateRoute中咱們會更新當前的路由(history.current), 並執行cb(更新Vue實例上的_route屬性,🌟這會觸發RouterView的從新渲染)
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
// 執行after的鉤子
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
複製代碼
接着咱們執行transitionTo的回調函數onComplete。在回調中會調用replaceHash或者pushHash方法。它們會更新location的hash值。若是兼容historyAPI,會使用history.replaceState或者history.pushState。若是不兼容historyAPI會使用window.location.replace或者window.location.hash。而handleScroll方法則是會更新咱們的滾動條的位置咱們這裏就不在細說了。
// replaceHash方法
(route) => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}
// push方法
route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}
複製代碼
好了,如今咱們就把,replace或者push方法的流程說完了。
🎉🎉🎉🎉🎉🎉 如下是transitionTo,confirmTransition中完整的代碼。 🎉🎉🎉🎉🎉🎉
// onComplete 導航成功的回調
// onAbort 導航終止的回調
transitionTo (location, onComplete, onAbort) {
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) })
}
}
)
}
// onComplete 導航成功的回調
// onAbort 導航終止的回調
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) })
}
}
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 = [].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() })
})
}
}
)
})
}
複製代碼
在VueRouter上定義的go,forward,back方法都是調用history的屬性的go方法
// index.js
go (n) {
this.history.go(n)
}
back () {
this.go(-1)
}
forward () {
this.go(1)
}
複製代碼
而hash上go方法調用的是history.go,它是如何更新RouteView的呢?答案是hash對象在setupListeners方法中添加了對popstate或者hashchange事件的監聽。在事件的回調中會觸發RoterView的更新
// go方法調用history.go
go (n) {
window.history.go(n)
}
複製代碼
咱們在經過點擊後退, 前進按鈕或者調用back, forward, go方法的時候。咱們沒有主動更新_app.route和current。咱們該如何觸發RouterView的更新呢?經過在window上監聽popstate,或者hashchange事件。在事件的回調中,調用transitionTo方法完成對_route和current的更新。
或者能夠這樣說,在使用push,replace方法的時候,hash的更新在_route更新的後面。而使用go, back時,hash的更新在_route更新的前面。
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)
}
})
})
}
複製代碼
HistoryRouter的實現基本於HashRouter一致。差別在於HistoryRouter不會作一些容錯處理,不會判斷當前環境是否支持historyAPI。默認監聽popstate事件,默認使用histroyAPI。感興趣的同窗能夠看/history/html5.js中關於HistoryRouter的定義。
RouterView是能夠互相嵌套的,RouterView依賴了parent.route即this._routerRoot._route。咱們使用Vue.util.defineReactive將_router設置爲響應式的。在transitionTo的回調中會更新_route, 這會觸發RouteView的渲染。(渲染機制目前不是很瞭解,目前尚未看過Vue的源碼,猛男落淚)。
export default {
name: 'RouterView',
functional: true,
// RouterView的name, 默認是default
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
data.routerView = true
// h爲渲染函數
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循環找到Vue的根節點, _routerRoot是Vue的根實例
// depth爲當前的RouteView的深度,由於RouteView能夠互相嵌套,depth能夠幫組咱們找到每一級RouteView須要渲染的組件
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]
// registerRouteInstance會在beforeCreated中調用,又全局的Vue.mixin實現
// 在matched.instances上註冊組件的實例, 這會幫助咱們修正confirmTransition中執行路由守衛中內部的this的指向
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)
}
}
複製代碼
咱們把VueRouter源碼看完了。整體來講不是很複雜。總的來講就是使用Vue.util.defineReactive將實例的_route屬性設置爲響應式。而push, replace方法會主動更新屬性_route。而go,back,或者點擊前進後退的按鈕則會在onhashchange或者onpopstate的回調中更新_route,而_route的更新會觸發RoterView的從新渲染
可是也略過了好比keep-live,滾動行爲的處理。我打算接下來,結合VueRouter核心原理實現了一個簡易版的VueRouter,固然如今尚未開始。
從3月中下旬左右一直在學一些庫的源碼,自己學習源碼對工做幫助並非很大。由於像VueRouter,Preact都有着完善的文檔。看源碼單純是我的的興趣,不過學習了這些庫的源碼,本身實現一個簡易版本,仍是挺有成就感的一件事情。