簡述vue-router實現原理

router源碼解讀

閱讀請關注下代碼註釋html

打個廣告:哪位大佬教我下sf怎麼排版啊,不會弄菜單二級導航(撲通.gif)前端

logo.png

<h2>1. router是什麼</h2>vue

首先,你會從源碼裏面引入Router,而後再傳入參數實例化一個路由對象node

// router/index.js
import Router from 'vue-router'
new Router({...})
...

源碼基礎類:vue-router

// 源碼index.js
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'   // 不選擇模式會默認使用hash模式
    this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
    if (this.fallback) {
      mode = 'hash'
    }
    if (!inBrowser) {         // 非瀏覽器環境默認nodejs環境
      mode = 'abstract'
    }
    this.mode = mode

    switch (mode) { // 根據參數選擇三種模式的一種
      case 'history':
        this.history = new HTML5History(this, options.base) // 根據HTML5版History的方法和屬性實現的模式
        break
      case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback) // 利用url中的hash特性實現
        break
      case 'abstract':
        this.history = new AbstractHistory(this, options.base) // 這種模式原理暫不清楚
        break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }
  }
  ...
  // 一些api方法,你應該很熟悉,$router.push(...)
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.push(location, onComplete, onAbort)
  }

  replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    this.history.replace(location, onComplete, onAbort)
  }

  go (n: number) {
    this.history.go(n)
  }

  back () {
    this.go(-1)
  }

  forward () {
    this.go(1)
  }
  ...
}

咱們建立的路由都是VueRouter類的實例化,用來管理咱們的【key-components-view】,一個key(代碼中的path)對應一個組件,view也就是<router-view>在template裏面佔個坑,用來根據key展現對應的組件,實例上的func讓咱們能夠控制路由,也就是官網的api
說簡單點,路由就是一個‘輪播圖’,emmmmmm,說輪播好像也不過度哈,寫個循環切換key的func就是‘輪播了’,而key就是輪播的index,手動滑稽。那麼,vue-router是如何實現不發送請求就更新視圖的呢,讓咱們來看看vue如何使用路由的
實例化後的路由輸出:區分下route和router
clipboard.pngapi

2. router工做原理
若是你要使用到router,你會在實例化Vue的參數options中加入router數組

// main.js
improt xxx from xxx
import router from xxx
new Vue({
  el: '#app',
  router: router,
  components: { App },
  template: '<App/>'
})

那,Vue是如何使用這個參數呢,vue-router是做爲插件加入使用的,經過mixin(混合)來影響每個Vue實例化,在beforeCreate 鉤子的時候就會完成router的初始化,從參數獲取router -> 調用init初始化 -> 加入響應式(defineReactive方法在vue源碼用的不少,也是底層實現響應式的核心方法)clipboard.png瀏覽器

// 源碼install.js
Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router   // 獲取options裏面的router配置
        this._router.init(this)               // 初始化,這個init是VueRouter類裏面的方法,實例化後會繼承這個方法,方法代碼見下方 
        Vue.util.defineReactive(this, '_route', this._router.history.current) // 這個是把_route加入數據監控,因此你能夠watch到_route
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

初始化會作些什麼:
-判斷主程序狀態(已經初始化了的vue實例不會再從新初始化路由,也就是你不能手動去再init一次)
-把實例加入內置數組
-判斷history的類型,作一些相似優化的事,好比hash模式的setupListeners方法,就是延遲監聽hashChange事件,等到vue完成掛載再監聽,太細節不用深刻
-listen定義一個callback,listen是定義在最底層History類上的,做用就是定義一個callback,listen會在須要的時候被調用,在路由發生變化的時候會執行這個callback網絡

// 源碼index.js
export default class VueRouter {
...
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)   // 這個apps存儲了讓全部的Vue實例化(根組件),後面遍歷的時候,會把當前標記route掛到全部根組件的,也就是 vm._route 也是 vm._router.history.current

    // main app already initialized.
    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 => {               // 注意這個listen會在後面用到
      this.apps.forEach((app) => {
        app._route = route                  // 根組件所有獲取當前route
      })
    })
  }
...
}

關於route的變化過程會在下面具體模式中說明,這裏先跳過,接下來先說vue拿到router後,怎麼使用router來渲染組件的app

3. vue如何使用router的

在安裝vue-router插件的時候

export function install (Vue) {
  ...
  Vue.component('RouterView', View)  // <router-link> & <router-view> 你應該很熟悉,本質就是vue組件,看源碼以前個人猜想也是組件
  Vue.component('RouterLink', Link)
  ...
}

router-link你不必定會使用,可是router-view你確定會使用,它就是做爲'窗口'的存在,來渲染你須要展現的組件。
那,從這個組件開始說,一個前提條件是:vnode是經過render來建立的,也就是說改變_route的值會執行render函數,Router-View這個組件定義了本身的render,省略了大部分代碼,這兩行夠了,你最終經過<router-view>看到的視圖就是這麼來的

// vue源碼render.js
export function renderMixin (Vue: Class<Component>) {
...
vnode = render.call(vm._renderProxy, vm.$createElement)
...
}
// router源碼 view.js
render (_, { props, children, parent, data }) {
...
const h = parent.$createElement
...
return h(component, data, children)
}

第一種:hashHistory模式

流程

$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()

1. 關於hash
url中#號後面的參數,別名哈希值,關於hash的一些特性

1.改變hash並不會引發頁面重載
2.HTTP請求不包括#,因此使用hash不會影響到其餘功能
3.改變#會改變瀏覽器的訪問歷史
4.window.location.hash能夠讀取哈希值
5.JavaScript能夠經過onhashchange監聽到hash值的變化,這就意味着能夠知道用戶在瀏覽器手動改變了hash值

clipboard.png

clipboard.png

由於這些特性纔有的hashHistory
更多關於hash知識見 URL的井號 - 阮一峯的網絡日誌

2. hashHistory源碼
首先,這三種模式都是經過繼承一個基礎類History來的

export class HashHistory extends History {
...
}

那,三種模式確定有相同的屬性,相同的方法,確定不會去建立三次因此從一個基類繼承,而後各自的部分屬性or方法會有差別,至於History這個類,我是不會去細看的,反正我也看不懂,哈哈哈哈

clipboard.png

router上的實例屬性、方法能夠在VueRouter、HashHistory/HTML5History/AbstractHistory、History上找到,這裏說下HashHistory的幾個func的實現、

// router源碼hash.js
export class HTML5History extends History {
...
go (n: number) {
    window.history.go(n)
  }
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
    const { current: fromRoute } = this
    this.transitionTo(location, route => {  // History類上的func
      pushHash(route.fullPath)
      handleScroll(this.router, route, fromRoute, false)
      onComplete && onComplete(route)
    }, onAbort)
  }

function pushHash (path) {
  if (supportsPushState) { // 是否瀏覽器環境且環境支持pushstat方法,這個func下面會說
    pushState(getUrl(path)) // 支持的話往window.history添加一條數據
  } else {
    window.location.hash = path // 不支持的話直接修改location的hash
  }
}

  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)
  }
// 其實replace和push只有兩個區別
1.
window.location.hash = path
window.location.replace(getUrl(path))
2.
if (replace) { // replace調這個func會傳一個true
  history.replaceState({ key: _key }, '', url)
} else {
  _key = genKey()
  history.pushState({ key: _key }, '', url)
}
...
}

還有一點就是,在初始化hash模式路由的時候,會執行一個func,監聽hashchange事件

setupListeners () {
    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)
        }
      })
    })
}

第二種:HTML5History模式

HTML5--History 科普

主要是新增的兩個api

1.History.pushState()

clipboard.png
[優勢寫的清清楚楚]
clipboard.png

clipboard.png

HTML5History的push、replace跟hash模式的差很少,就不上代碼了
一個標記是否支持HTML5的flag,這麼寫的,有須要的能夠刨回去用

export const supportsPushState = inBrowser && (function () {
  const ua = window.navigator.userAgent

  if (
    (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
    ua.indexOf('Mobile Safari') !== -1 &&
    ua.indexOf('Chrome') === -1 &&
    ua.indexOf('Windows Phone') === -1
  ) {
    return false
  }

  return window.history && 'pushState' in window.history
})()

還有一個就是scrollBehavior,用來記錄路由跳轉的時候滾動條的位置,這個只能在HTML5模式下使用,即支持pushState方法的時候,部分博客說只有在HTML5History下才能使用,這個等我明天驗證一下,我我的以爲支持HTML5就能夠了

2.History.replaceState()

clipboard.png
說的也很直觀,就是不創新新紀錄而覆蓋一條記錄,just do it

結束語

別問第三種狀況(我是誰、我在哪、誰打我)

我兜子好沃,早知道不作前端了~

在學習router源碼的時候閱讀了熵與單子的代碼本的文章,看完這篇文章配合源碼基本均可以很好掌握vue-router的大概,感謝做者,另外說明下本文由本人學習結束後加上本身的理解一字一字敲出來的,可能有些類似之處,侵刪請聯繫我,寫文章的目的是看看本身可否表述清楚,對知識點的掌握狀況,講的不對的地方,請各位大佬指正~

~感謝潘童鞋的指導(^▽^)

固然,我也稀罕你的小❤❤,點個贊再走咯~

以上圖片均來自MDN網頁截圖、vue官網截圖、百度首頁截圖,不存在版權問題 /滑稽

【注】:內容有不當或者錯誤處請指正~轉載請註明出處~謝謝合做!

相關文章
相關標籤/搜索