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屬性,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都有着完善的文檔。看源碼單純是我的的興趣,不過學習了這些庫的源碼,本身實現一個簡易版本,仍是挺有成就感的一件事情。