看完拉勾前端訓練營關於Vue-Router的實現,乾貨滿滿,但Vue-Router的實現實在是繞,因此作一下筆記,確認以及加深本身的瞭解。進了拉勾前端訓練營兩個多月,收穫仍是挺多的,羣裏很多大牛,還有美女班主任,導師及時回答學員的疑問,幽默風趣,真是羣裏一席談,勝讀四年本科(literally true,四年本科的課程真的水=_=)。html
實現前,看一下實現的功能:前端
建立一個項目。首先確定是要建立Vue Router的類,在根目錄下建立index.js文件:vue
export default class VueRouter {constructor (option) {this._routes = options.routes || [] } init () {} }複製代碼
咱們平時建立路由實例時,會傳入一個對象,像這樣:html5
const router = new VueRouter({ routes })複製代碼
因此構造函數應該要有一個對象,若是裏面有路由routes,賦值給this._routes,不然給它一個空數組。options裏固然有其餘屬性,但先無論,以後再實現。 還有一個init方法,用來初始化設定。設計模式
因爲Vue Router是插件,要想使用它,必須經過Vue.use方法。該方法會斷定傳入的參數是對象還函數,若是是對象,則調用裏面的install方法,函數的話則直接調用。 Vue Router是一個對象,因此要有install方法。實現install以前,看一下Vue.use的源碼,這樣能夠更好理解怎樣實現install:數組
export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) {const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))if (installedPlugins.indexOf(plugin) > -1) { return this}const args = toArray(arguments, 1) args.unshift(this)if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) } installedPlugins.push(plugin)return this } }複製代碼
首先Vue.use會先斷定Vue有沒有一個屬性叫_installedPlugins,有則引用,沒有就爲Vue添加屬性_installedPlugins,它是一個空數組,再去引用它。_installedPlugins是記錄安裝過的插件。接下來斷定_installedPlugins裏有沒有傳入的插件,有則不用安裝。 把傳入的參數從第二個開始,變成數組,把Vue放入數組首位。若是插件是對象,則調用它的install方法,插件方法裏的上下文this依然是它自身,傳入剛纔變成數組的參數。函數的話,不用考慮上下文,直接調用。最後記錄該插件是安裝過的。app
如今簡單把install方法實現,在根目錄下新建install.js:ide
export let _Vue = nullexport default function install (Vue) { _Vue = Vue _Vue.mixin({ beforeCreate () { if (this.$options.router) {this._router = this.$options.routerthis._routerRoot = this// 初始化 router 對象this._router.init(this) } else {this._routerRoot = this.$parent && this.$parent._routerRoot } } })複製代碼
全局變量_Vue是爲了方便其餘Vue Router模塊的引用,否則的話其餘模式須要引入Vue,比較麻煩。mixin是把Vue中某些功能抽取出來,方便在不一樣地方複用,這裏的用法是全局掛載鈎子函數。函數
先判斷是否爲根實例,若是是根實例,會有路由傳入,因此會$options.router存在。根實例的話則添加兩個私有屬性,其中_routerRoot是爲了方便根實例如下的組件引用,而後初始化router。若是是根實例下的組件,去找一下有沒有父組件,有就引用它的_routerRoot,這樣能夠經過_routerRoot.router來引用路由。工具
掛載函數基本完成。當咱們使用Vue Router,還有兩個組件掛載:Router Link和Router View。在根目錄下建立文件夾components,建立文件link.js和view.js。先把Router Link實現:
export default { name: 'RouterLink', props: {to: { type: String, required: true} }, render (h) {return h('a', { attrs: { href: '#' + this.to } }, [this.$slots.default]) } }複製代碼
RouterLink接收一個參數to,類型是字符串。這裏不使用template,是由於運行版本的vue沒有編譯器,把模板轉爲渲染函數,要直接用渲染函數。 簡單講一下渲染函數的用法,第一個參數是標籤類型,第二個是標籤的屬性,第三是內容。詳細能夠看vue文檔。 咱們要實現的實際上是<a :href="{{ '#' + this.to }}"><slot name="default"></slot></a>。因此第一個參數是a,第二個它的鏈接,第三個之因此要用數組,是由於標籤的內容是一個slot標籤節點,子節點要用數組包起來。 至於RouterView,如今不知道它的實現,大概寫一下:
export default { name: 'RouterView', render (h) {return h () } }複製代碼
在install裏把兩個組件註冊:
import Link from './components/link'import View from './components/view'export default function install (Vue) { ... _Vue.component(Link.name, Link) _Vue.component(View.name, View) }複製代碼
接下來要建立create-matcher,它是用來生成匹配器,主要返回兩個方法:match和addRoutes。前者是匹配輸入路徑,獲取路由表相關資料,後者是手動添加路由規則到路由表。這兩個方法都是要依賴路由表,因此咱們還要實現路由表生成器:create-router-map,它接收路由規則,返回一個路由表,它是對象,裏面有兩個屬性,一個是pathList,它是一個數組,存有全部路由表的路徑,另外一個是pathMap,是一個字典,鍵是路徑,而值的路徑相應的資料。 在項目根目錄下建立create-router-map.js:
export default function createRouteMap (routes) { // 存儲全部的路由地址 const pathList = [] // 路由表,路徑和組件的相關信息 const pathMap = {} return { pathList, pathMap } }複製代碼
咱們須要遍歷路由規則,在這過程當中作兩件事:
這裏的難點是有子路由,因此要用遞歸,但如今先不要考慮這問題,簡單把功能實現:
function addRouteRecord (route, pathList, pathMap, parentRecord) { const path = route.path const record = {path: path,component: route.component,parentRecord: parentRecord// ... } // 判斷當前路徑,是否已經存儲在路由表中了 if (!pathMap[path]) { pathList.push(path) pathMap[path] = record } }複製代碼
如今考慮一會兒路由的問題。首先要先有斷定路由是否有子路由,有的話遍歷子路由,遞歸處理,還要考慮路徑名稱問題,若是是子路由,path應該是父子路徑合併,因此這裏要斷定是否存有父路由。
function addRouteRecord (route, pathList, pathMap, parentRecord) { const path = parentRecord ? `${parentRecord.path}/${route.path}` : route.path const record = {path: path,component: route.component,parentRecord: parentRecord// ... } // 判斷當前路徑,是否已經存儲在路由表中了 if (!pathMap[path]) { pathList.push(path) pathMap[path] = record } // 判斷當前的route是否有子路由 if (route.children) { route.children.forEach(childRoute => { addRouteRecord(childRoute, pathList, pathMap, route) }) } }複製代碼
若是有傳入父路由資料,path是父子路徑合併。
最後把addRouteRecord添加到createRouteMap:
export default function createRouteMap (routes) { // 存儲全部的路由地址 const pathList = [] // 路由表,路徑和組件的相關信息 const pathMap = {} // 遍歷全部的路由規則 routes routes.forEach(route => { addRouteRecord(route, pathList, pathMap) }) return { pathList, pathMap } }複製代碼
createRouteMap實現了,能夠把create-matcher的路由表建立和addRoute實現:
import createRouteMap from './create-route-map'export default function createMatcher (routes) { const { pathList, pathMap } = createRouteMap(routes) function addRoutes (routes) { createRouteMap(routes, pathList, pathMap) } return { match, addRoutes } }複製代碼
最後要實現match了,它接收一個路徑,而後返回路徑相關資料,相關資料不只僅是它自身的,還有它的父路徑的資料。這裏先實現一個工具類函數,它是專門建立路由的,就是返回路徑以及它的相關資料。建立util/route.js:
export default function createRoute (record, path) { // 建立路由數據對象 // route ==> { matched, path } matched ==> [record1, record2] const matched = [] while (record) { matched.unshift(record) record = record.parentRecord } return { matched, path }複製代碼
其實功能很簡單,就是不斷獲取上一級的資料,放進數組首位。配上createRoute,match基本就實現了:
import createRoute from './util/route' function match (path) {const record = pathMap[path]if (record) { // 建立路由數據對象 // route ==> { matched, path } matched ==> [record1, record2] return createRoute(record, path) }return createRoute(null, path) }複製代碼
在VueRouter的構造函數裏把matcher加上:
import createMatcher from './create-matcher'export default class VueRouter { constructor (options) {this._routes = options.routes || []this.matcher = createMatcher(this._routes) ...複製代碼
matcher作好後,開始實現History類吧,它的目的是根據用戶設定的模式,管理路徑,通知 RouterView把路徑對應的組件渲染出來。
在項目根目錄新建history/base.js:
import createRoute from '../util/route'export default class History { constructor (router) {this.router = router// 記錄當前路徑對應的 route 對象 { matched, path }this.current = createRoute(null, '/') } transitionTo (path, onComplete) {this.current = this.router.matcher.match(path) onComplete && onComplete() } }複製代碼
建立時當時路徑先默認爲根路徑,current是路由對象,屬性有路徑名和相關資料,transitionTo是路徑跳轉時調用的方法,它更改current和調用回調函數。 以後不一樣模式(如hash或history)的類都是繼承History。這裏只實現HashHistory:
import History from './base'export default class HashHistory extends History { constructor (router) {super(router)// 保證首次訪問的時候 #/ensureSlash() } getCurrentLocation () {return window.location.hash.slice(1) } setUpListener () {window.addEventListener('hashchange', () => { this.transitionTo(this.getCurrentLocation()) }) } }function ensureSlash () { if (window.location.hash) {return } window.location.hash = '/'}複製代碼
HashHistory基本是圍繞window.location.hash,因此先講一下它。簡單來講,它會返回#後面的路徑名。若是對它賦值,它會在最前面加上#。明白window.location.hash後,其餘方法都不難理解。setUpListener註冊一個hashchange事件,表示當哈希路徑(#後的路徑)發生變化,調用註冊的函數。
html5模式不實現了,繼承HashHistory算了:
import History from './base'export default class HTML5History extends History { }複製代碼
History的類基本實現了,可是如今還不是響應式的,意味着即便實例發生變化,視圖不會變化。這問題後解決。
回到VueRouter的構造函數:
constructor(options) ...const mode = this.mode = options.mode || 'hash'switch (mode) { case 'hash':this.history = new HashHistory(this)break case 'history':this.history = new HTML5History(this)break default:throw new Error('mode error') } }複製代碼
這裏使用了簡單工廠模式 (Simple Factory Pattern),就是設計模式中工廠模式的簡易版。它存有不一樣的類,這些類都是繼承同一類的,它經過傳入的參數進行判斷,建立相應的實例返回。簡單工廠模式的好處是用戶不用考慮建立實例的細節,他要作的是導入工廠,往工廠傳入參數,就可得到實例。
以前的History有一個問題,就是它不是響應式的,也就是說,路徑發生變化,瀏覧器不會有任何反應,要想爲響應式,能夠給它一個回調函數:
import createRoute from '../util/route'export default class History { constructor (router) { ...this.cb = null } ... listen (cb) {this.cb = cb } transitionTo (path, onComplete) {this.current = this.router.matcher.match(path)this.cb && this.cb(this.current) onComplete && onComplete() } }複製代碼
加上listen方法,爲History添加回調函數,當路徑發生轉變時調用。
把以前的初始化方法init補上:
init (app) { // app 是 Vue 的實例 const history = this.history history.listen(current => { app._route = current }) history.transitionTo( history.getCurrentLocation(), history.setUpListener ) }複製代碼
給history的回調函數是路徑發生變化,把路由傳給vue實例,而後是轉換至當前路徑,完成時調用history.setUpListener。不過直接把history.setUpListener放進去有一個問題,由於這等因而僅僅把setUpListener放進去,裏面的this指向window,因此要用箭頭函數封裝,這樣的話,就會調用history.setUpListener,this指向history。
init (app) {// app 是 Vue 的實例const history = this.historyconst setUpListener = () => { history.setUpListener() } history.listen(current => { app._route = current }) history.transitionTo( history.getCurrentLocation(), setUpListener ) }複製代碼
用箭頭函數把history.setUpListener封裝一下,this就指向history。
init完成實現,回來把install的剩餘地方實現了。當初始化完成後,把vue實例的路由(不是路由表)變成響應式,可使用 Vue.util.defineReactive(this, '_route', this._router.history.current),就是爲vue實例添加一個屬性_route,它的值是this._router.history.current,最後添加router和route。 完整代碼以下:
import Link from './components/link'import View from './components/view'export let _Vue = nullexport default function install (Vue) { // 判斷該插件是否註冊略過,能夠參考源碼 _Vue = Vue // Vue.prototype.xx _Vue.mixin({ beforeCreate () { // 給全部 Vue 實例,增長 router 的屬性 // 根實例 // 以及全部的組件增長 router 屬性 if (this.$options.router) {this._router = this.$options.routerthis._routerRoot = this// 初始化 router 對象this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current)// this.$parent// this.$children } else {this._routerRoot = this.$parent && this.$parent._routerRoot } } }) _Vue.component(Link.name, Link) _Vue.component(View.name, View) Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) }複製代碼
如今就能夠如平時開發同樣,使用router和route。
最後把RouterView實現。其實它也沒什麼,就是獲取當取路徑,從路徑中獲得組件,而後渲染出來。問題是要考慮父子組件的問題。把思想整理一下,當有父組件時,確定是父組件已經渲染出來,子組件是從父組件的RouterView組件渲染,還有是$route有的是當前路徑和匹配的資料的數組,即包括父組件的數組,因此可遍歷得到要渲染的組件:
export default { name: 'RouterView', render (h) {const route = this.$routelet depth = 0//routerView表示已經完成渲染了this.routerView = truelet parent = this.$parentwhile (parent) { if (parent.routerView) { depth++ } parent = parent.$parent }const record = route.matched[depth]if (record) { return h(record.component) }return h() } }複製代碼
if (parent.routerView)
是由於是確認父組件是否已經渲染,若是渲染,它的routerView爲true,用depth來記錄有多少父路由,而後經過它獲取matched的資料,有的話則渲染獲取的組件。
Vue Router的代碼量很少,但實在是繞,簡單總結一下比較好。先看一下項目結構:
用一張表把全部的文件做用簡述一遍:
文件 | 做用 |
---|---|
index.js | 存放VueRouter類 |
install.js | 插件類必需要有的函數,用來給Vue.use調用 |
create-route-map.js | 生成路由表,它輸出一個對象,有pathList和pathMap屬性,前者是存有全部路徑的數組,後者是字典,把路徑和它的資料對應 |
util/route.js | 一個函數接收路徑爲參數,返回路由對象,存有matched和path屬性,matched是匹配到的路徑的資料和父路徑資料,它是一個數組,path是路徑自己 |
create-matcher.js | 利用create-route-map建立路由表,且返回兩個函數,一個是用util/route匹配路由,另外一個是手動把路由規則轉變成路由 |
history/base.js | History類文件,用來做歷史管理,存有當前路徑的路由,以及轉換路徑的方法 |
history/hash.js | HashHistory類文件,繼承至History,用做hash模式下的歷史管理 |
components/link.js | Router-Link的組件 |
components/view.js | Router-View的組件 |