Vue-Router實現

看完拉勾前端訓練營關於Vue-Router的實現,乾貨滿滿,但Vue-Router的實現實在是繞,因此作一下筆記,確認以及加深本身的瞭解。進了拉勾前端訓練營兩個多月,收穫仍是挺多的,羣裏很多大牛,還有美女班主任,導師及時回答學員的疑問,幽默風趣,真是羣裏一席談,勝讀四年本科(literally true,四年本科的課程真的水=_=)。html

實現的功能

實現前,看一下實現的功能:前端

  1. 基本路由功能
  2. 子路由功能
  3. History及Hash功能

建立一個項目。首先確定是要建立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方法,用來初始化設定。設計模式

install

因爲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)
}複製代碼

createMatcher

接下來要建立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
  }
}複製代碼

咱們須要遍歷路由規則,在這過程當中作兩件事:

  1. 把全部路徑存入pathList
  2. 把路由和資料對應關係放入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)
...複製代碼

History歷史管理

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),就是設計模式中工廠模式的簡易版。它存有不一樣的類,這些類都是繼承同一類的,它經過傳入的參數進行判斷,建立相應的實例返回。簡單工廠模式的好處是用戶不用考慮建立實例的細節,他要作的是導入工廠,往工廠傳入參數,就可得到實例。

init

以前的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。

install補完

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實現。其實它也沒什麼,就是獲取當取路徑,從路徑中獲得組件,而後渲染出來。問題是要考慮父子組件的問題。把思想整理一下,當有父組件時,確定是父組件已經渲染出來,子組件是從父組件的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的代碼量很少,但實在是繞,簡單總結一下比較好。先看一下項目結構:

ProjectStructure

用一張表把全部的文件做用簡述一遍:

文件 做用
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的組件
相關文章
相關標籤/搜索