主頁 > 企業開發 > Vue Router 原始碼分析

Vue Router 原始碼分析

2023-06-30 08:22:12 企業開發

專欄分享:vue2原始碼專欄,玩具專案專欄,硬核 ?? 推薦 ??

歡迎各位 ITer 關注點贊收藏 ??????

本篇文章參考版本:vue-router v3.x
最終成果,實作了一個可運行的核心路由工程:柏成/vue-router3.x
目錄結構如下:

.
|-- components           // 組件(view/link)
|   |-- router-link.js
|   `-- router-view.js
|-- create-matcher.js    // Route 匹配
|-- create-route-map.js  // Route 映射表
|-- history              // Router 處理 (hash模式、history模式)
|   |-- base.js
|   |-- hash.js
|   `-- html5.js
|-- index.js             // Router 類
`-- install.js           // Router 插件安裝

1. 路由注冊

起步

我們先來看一個基本例子,熟悉其簡單的應用配置

import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

// 1. 定義 (路由) 組件
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

// 2. 定義路由
const routes = [
  { path: '/foo', component: Foo },
  { path: '/bar', component: Bar }
]

// 3. 創建 router 實體,然后傳 routes 配置
const router = new VueRouter({
  mode: 'history',
  routes
})

// 4. 創建和掛載根實體,記得注入路由器,從而讓整個應用都有路由功能
const app = new Vue({
  router
}).$mount('#app')

通過注入路由器,我們可以在任何組件內通過 this.$router 訪問路由器,也可以通過 this.$route 訪問當前路由(后面我們會詳細介紹其內部實作)

use

我們發現,如果要在一個模塊化工程中使用 vue-router,必須要通過 Vue.use() 明確地安裝路由功能
Vue.use(plugin) 是一個全域插件注冊API,官方是這樣介紹的:

安裝 Vue.js 插件時呼叫,如果插件是一個物件,必須提供 install 方法,如果插件是一個函式,它本身會被作為 install 方法,install 方法呼叫時,會將 Vue 物件作為第一個引數傳入

將 Vue 物件當做引數的好處就是插件的撰寫方不需要額外 import Vue 了,可減少插件包的體積

install

vue-router 的入口檔案是 src/index.js,其中定義了 VueRouter 類;也實作了 install 的靜態方法,它的定義在 src/install.js

Vue.use(VueRouter) 默認會呼叫 VueRouter 類上的 install 方法

src/index.js:

import install from './install'
export default class VueRouter{ }
VueRouter.install = install

src/install.js:

import routerLink from './components/router-link'
import routerView from './components/router-view'

// 靜態全域變數
export let Vue

function install (_Vue) {
  Vue = _Vue

  // mixin 內部會呼叫 mergeOptions方法, 所有組件初始化都會呼叫這個方法
  // 這里不能直接將屬性定義在原型上, 只有在 new Vue 中傳入了 router路由實體 才能被后代組件共享
  Vue.mixin({
    beforeCreate () {
      // 組件渲染是從父到子的
      // 這樣保證了有 router路由實體才加,沒有 router路由實體就不加
      if (this.$options.router) {
        this._routerRoot = this // 根實體
        this._router = this.$options.router // router路由實體

        this._router.init(this) // this 就是我們的根應用 new Vue()

        // 給根實體添加一個屬性 _route,值就是當前的 current物件,并將 this._route變成了回應式物件(資料變化應該會引起頁面重新渲染)
        // 注意!!!current 改變并不會觸發 _route的改變,我們需要在 current變化時手動更新 this._route的值
        // let current = {}; let _route = current; current = {name: '新的'}; _route 仍然是 {}
        Vue.util.defineReactive(this, '_route', this._router.history.current)

        // this._router 可以拿到路由實體
        // this._route 可以拿到current物件
      } else {
        // 在所有后代組件上都增加 _routerRoot,其指向根實體
        this._routerRoot = this.$parent && this.$parent._routerRoot
      }
    }
  })

  // 代理實體上的 $router 屬性,this.$router
  Object.defineProperty(Vue.prototype, '$router', {
    get () {
      return this._routerRoot && this._routerRoot._router
    }
  })
  // 代理實體上的 $route 屬性,this.$route
  Object.defineProperty(Vue.prototype, '$route', {
    get () {
      return this._routerRoot && this._routerRoot._route
    }
  })

  // 注冊 router-link 全域組件
  Vue.component('router-link', routerLink)
  // 注冊 router-view 全域組件
  Vue.component('router-view', routerView)
}
export default install

在 install 方法中,我們主要做了 3 件事 !

  1. 我們利用 Vue.mixin 將 beforeCreate 生命周期鉤子注入到了每一個組件中,并在組件自身鉤子之前呼叫

    在 beforeCreate 鉤子中,我們將根實體 _routerRoot 共享給了所有的后代組件;給根實體添加了一個 _router 屬性(VueRouter實體)并呼叫了 router 的初始化方法 this._router.init();然后用 defineReactive 方法給根實體添加了一個回應式屬性 _route(當前路由物件)

  2. 在 Vue 原型上代理了 $router$route 屬性,這就是為什么我們可以在任何組件內通過 this.$router 訪問路由器、通過 this.$route 訪問當前路由

  3. 通過 Vue.component 注冊了全域組件 <router-link><router-view>

下一節我們將分析一下 VueRouter 物件的實作和它的初始化作業

2. VueRouter 類

VueRouter 的實作是一個類,在入口檔案 src/index.js中定義

import install from './install'
import createMatcher from './create-matcher'
import HashHistory from './history/hash'
import Html5History from './history/html5'

class VueRouter {
  constructor (options) {
    // 用戶傳遞的路由配置
    const routes = options.routes || []
    this.beforeEachHooks = []

    // 路由匹配器,可以匹配也可以添加新的路由
    this.matcher = createMatcher(routes)

    const mode = options.mode || 'hash'
    if (mode === 'hash') {
      this.history = new HashHistory(this) // popstate, hashchange
    } else if (mode === 'history') {
      this.history = new Html5History(this) // popstate
    }
  }

  // 路由守衛,快取回呼鉤子,在 transitionTo方法中執行回呼鉤子
  beforeEach (cb) {
    this.beforeEachHooks.push(cb)
  }

  // router初始化方法(只會在 根vue實體中的 beforeCreate鉤子中呼叫一次)
  init (app) {
    console.log('router初始化方法(init)')
    const history = this.history
    // 手動根據當前路徑去匹配對應的組件,渲染,之后監聽路由變化
    history.transitionTo(history.getCurrentLocation(), () => {
      history.setupListener()
    })

    // 在 transitionTo 方法中執行這個回呼,目的就是在 current變化時手動更新 app._route的值,資料變化會自動重新渲染視圖
    history.listen((newRoute) => {
      app._route = newRoute
    })
  }

  // 簡化用戶呼叫層級  this.match ≈ this.matcher.match
  match (location) {
    return this.matcher.match(location)
  }

  // 呼叫 HashHistory or Html5History 的跳轉邏輯(點擊router-link觸發)
  push (location) {
    // 針對hash模式: window.location.hash
    // 針對history模式: history.pushState
    return this.history.push(location)
  }
}

// 當執行 Vue.use(VueRouter) 時,如果 VueRouter插件是一個物件,必須提供 install方法,install方法呼叫時,會將 Vue作為引數傳入
VueRouter.install = install

export default VueRouter

constructor

我們先來分析一下 constructor建構式,看看當我們 new VueRouter({}) 時執行了哪些操作

在建構式中,我們定義了一些私有屬性, this.beforeEachHooks 屬性用來記錄路由守衛鉤子回呼(最終在 transitionTo 方法中執行回呼,后面會詳細介紹);this.matcher 屬性代表路由匹配器物件createMatcher() 方法回傳了 match、addRoute、addRoutes 等方法,可以匹配、添加新的路由(我們會在 transitionTo 方法中應用 match,后面會詳細介紹);this.history 屬性代表路由歷史實體,是具體執行各種路由操作的執行者,其根據選項模式 options.mode 的不同,去 new 了一個相對應的 History 實體 HashHistory or Html5History(后面會詳細介紹)

tip:transitionTo 方法的實作是在 src/history/base.js 中,他負責處理所有的跳轉邏輯,會根據路徑匹配對應的路由記錄,更新 app._route(回應式物件)為最新的路由物件,從而觸發 setter 劫持,通知 <router-view> 去渲染新的組件(先有個大概認知,后面會詳細介紹)

init

觸發時機:還記得嗎?在前面 VueRouter.install 方法中,我們利用 Vue.mixin 將 beforeCreate 生命周期鉤子注入到了每一個組件中,我們在 new Vue({}) 根實體時,會把 router 路由器注入到根實體,所以我們只會在根vue實體中的 beforeCreate 鉤子中呼叫一次router.init方法

src/install.js:

beforeCreate () {
  if (this.$options.router) {
    this._routerRoot = this // 根實體
    this._router = this.$options.router // router路由實體

    this._router.init(this) // this 就是我們的根應用 new Vue()

    Vue.util.defineReactive(this, '_route', this._router.history.current)
  } else {
    this._routerRoot = this.$parent && this.$parent._routerRoot
  }
}

我們在 router 初始化方法中主要做了兩件事!

  1. 執行 history.transitionTo 方法,根據當前路徑去匹配對應的組件,并渲染,然后通過 history.setupListener 在回呼中添加路由監聽器,當路由變化時,我們就可以做一些事情了!當然,不同的路由模式有不同的 setupListener 實作(后面會詳細介紹)
history.transitionTo(history.getCurrentLocation(), () => {
  history.setupListener()
})
  1. 執行 history.listen 方法,快取更新 app._route 的回呼,后續我們會在 transitionTo 方法中執行這個回呼,目的就是在 current 物件變化時手動更新一下 app._route 的值,
history.listen((newRoute) => {
  app._route = newRoute
})

我們之前用 defineReactive 方法將根實體上的 _route 屬性變成了一個回應式物件,其資料變化后,會觸發 setter 劫持,通知 <router-view> 去自動渲染新的組件

tip:為什么要手動更新 app._route 的值呢?因為 current 改變并不會觸發 _route 的改變,所以我們需要在 current 變化時手動更新一下 _route 的值,看個小例子就明白了

// Vue.util.defineReactive(this, '_route', this._router.history.current)

let current = {}; let _route = current; current = {name: '新值'}; // _route 仍然是 {}

push

當我們通過 <router-link> 跳轉路由時會觸發此方法,內部呼叫了 history 物件(路由歷史實體)的 push 方法,不同的 mode 選項有不同的實作方式

hash模式下,我們通過 transitionTo 方法匹配渲染新的組件,然后通過 history.pushState/location.hash (優雅降級處理)往路由堆疊中添加一條路由記錄(transitionTo 方法后面會詳細介紹)

HashHistory 類在 src/history/hash.js中實作

const supportsPushState = window.history && typeof window.history.pushState === 'function'

class HashHistory extends Base {
  push (location) {
    this.transitionTo(location, () => {
      if (supportsPushState) {
        window.history.pushState({}, '', getUrl(location))
      } else {
        window.location.hash = location
      }
    })
  }
}

history模式下,我們也是通過 transitionTo 方法匹配渲染新的組件,然后通過 history.pushState 往路由堆疊中添加一條路由記錄

Html5History 類在 src/history/html5.js中實作

class HTML5History extends Base {
  push (location) {
    this.transitionTo(location, () => {
      window.history.pushState({}, '', location)
    })
  }
}

下一節我們將分析一下 matcher 路由匹配器的實作

3. matcher

create-matcher

createMatcher 方法回傳了 match、addRoute、addRoutes 等方法,可以匹配、添加新的路由,他的定義在 src/create-matcher.js

其中 match 方法可以根據一個 location 路徑,去 createRouteMap 方法回傳的 pathMap 路由映射表中匹配到對應的路由資訊

 export default function createMatcher (routes) {
  // pathList:收集所有的路由路徑,['/', '/a', '/b', '/about', '/about/a', '/about/b']
  // pathMap:收集路徑的對應路由記錄,['/':{/的記錄}, '/a':{/a的記錄}, '/b':{/b的記錄}, '/about':{/about的記錄}, ...]
  const { pathList, pathMap } = createRouteMap(routes)

  // 動態添加多個路由規則 在v4.x中已廢棄:使用 router.addRoute() 代替
  function addRoutes (routes) {
    createRouteMap(routes, pathList, pathMap)
  }
  // 動態添加一條新路由規則
  function addRoute (route) {
    createRouteMap([route], pathList, pathMap)
  }
  // 根據一個路徑獲取對應的路由資訊 在v4.x中已廢棄:洗掉 router.match 改為 router.resolve
  function match (location) {
    return pathMap[location]
  }

  return {
    addRoutes,
    addRoute,
    match
  }
}

createMatcher 觸發時機:VueRouter 類的 constructor 建構式中,我們定義了一些私有屬性,其中就包括this.matcher 路由匹配器物件,VueRouter 類在 src/index.js 中實作

class VueRouter {
  constructor (options) {
    // 用戶傳遞的路由配置
    const routes = options.routes || []
    this.matcher = createMatcher(routes)
      ...
  }

  // 簡化用戶呼叫層級  this.match ≈ this.matcher.match
  match (location) {
    return this.matcher.match(location)
  }
}

match 應用時機:我們會在 transitionTo 方法中運行 match 方法,用以匹配對應的路由資訊,然后更新 app._route(回應式物件)為最新的路由物件,從而觸發 setter 劫持,通知 <router-view> 去渲染新的組件

create-route-map

createMatcher 創建路由匹配器時,會用到 createRouteMap 方法去創建路由映射關系,它的定義在 src/create-route-map.js

該方法根據用戶傳入的 routes選項,回傳了 pathList(收集所有的路由路徑) 和 pathMap(收集路徑的對應路由記錄,這就是我們的路由映射表) 2個物件

 // @return pathList:收集所有的路由路徑,['/', '/a', '/b', '/about', '/about/a', '/about/b']
 // @return pathMap:收集路徑的對應路由記錄,['/':{/的記錄}, '/a':{/a的記錄}, '/b':{/b的記錄}, '/about':{/about的記錄}, ...]
export default function createRouteMap (routes, pathList, pathMap) {
  // 當第一次加載的時候沒有 pathList 和 pathMap
  pathList = pathList || []
  pathMap = pathMap || {}
  routes.forEach(route => {
    addRouteRecord(route, pathList, pathMap)
  })

  return {
    pathMap
  }
}

// 添加路由資訊
function addRouteRecord (route, pathList, pathMap, parentRecord) {
  const path = parentRecord ? `${parentRecord.path}${parentRecord.path.endsWith('/') ? '' : '/'}${route.path}` : route.path
  const record = {
    path,
    component: route.component,
    props: route.props,
    meta: route.meta,
    parent: parentRecord
  }

  // 維護路徑對應的屬性
  if (!pathMap[path]) {
    pathList.push(path)
    pathMap[path] = record
  }

  route.children && route.children.forEach(childRoute => {
    addRouteRecord(childRoute, pathList, pathMap, record)
  })
}

下一節我們將分析一下 HashHistory(hash模式)、HTML5History(history模式)物件的實作

4. 路由模式

HashHistory 和 Html5History 的實作是兩個類,他們均繼承自 base 基類,由于 base 基類中主要是路由跳轉相關的邏輯,我們打算在下一章節和路由組件、導航守衛一起分析,它在 src/history/base.js中定義

HashHistory

hash 模式,優先使用 history.pushState/repaceState API 來完成 URL 跳轉和 onpopstate 事件監聽路由變化,不支持再降級為 location.hash API 和 onhashchange 事件,在src/history/hash.js中定義

import Base from './base'

const supportsPushState = window.history && typeof window.history.pushState === 'function'

class HashHistory extends Base {
  constructor (router) {
    super(router)

    // 初始化 hash路由時,給一個默認的 hash路徑 /
    ensureSlash()
  }

  // 獲取hash路徑片段 http://192.168.21.144/framework-assets#/assets/1522392838?id=1522392838 => '/assets/1522392838?id=1522392838'
  getCurrentLocation () {
    return getHash()
  }

  // 添加監聽器,監聽hash值的變化(在 vueRouter類的init方法中呼叫)
  // 當用戶在瀏覽器點擊后退、前進,或者在js中呼叫HTML5 history API【history.back(),history.go(),history.forward()】等,會觸發 popstate事件 和 hashchange事件
  // 用戶通過 location.hash = 'xxx' 也會觸發 popstate事件 和 hashchange事件
  // 但 history.pushState(),history.replaceState()不會觸發這兩個事件!!!
  setupListener () {
    const eventType = supportsPushState ? 'popstate' : 'hashchange'
    window.addEventListener(eventType, () => {
      this.transitionTo(getHash()) // 初始化執行的 ensureSlash方法也會觸發此回呼
    })
  }

  // 跳轉頁面
  // 為什么要手動執行 transitionTo,而不是直接改變地址,通過路由監聽器去間接執行 transitionTo?
  // 因為 window.history.pushState() 不會觸發 popstate事件!!!
  push (location) {
    this.transitionTo(location, () => {
      if (supportsPushState) {
        window.history.pushState({}, '', getUrl(location))
      } else {
        window.location.hash = location
      }
    })
  }
}

// http://localhost:8080/  ==>  http://localhost:8080/#/
function ensureSlash () {
  if (window.location.hash) {
    return
  }
  window.location.hash = '/'
}

// 獲取當前hash值(去掉 #)
// '#/assets/1522392838?id=1522392838'  ==>  '/assets/1522392838?id=1522392838'
function getHash () {
  return window.location.hash.slice(1)
}

// 絕對路徑
function getUrl (path) {
  const href = https://www.cnblogs.com/burc/archive/2023/06/29/window.location.href
  const i = href.indexOf('#')
  const base = i >= 0 ? href.slice(0, i) : href
  return `${base}#${path}`
}

export default HashHistory

ensureSlash

當我們實體化一個 history 物件時,會默認在 constructor 建構式中執行 ensureSlash 方法,如果沒有hash 值的話就給一個默認的 hash 路徑 /,確保存在 hash 錨點

其作用就是將 http://localhost:8080/ 自動修改為 http://localhost:8080/#/

setupListener

添加路由監聽器,當 hash 值變化時呼叫 transitionTo 方法統一處理跳轉邏輯,事件注冊采用了降級處理,優先使用 onpopstate 事件,若不支持,則降級使用 onhashchange 事件

當用戶點擊瀏覽器的后退、前進按鈕,在 js 中呼叫 HTML5 history API,如 history.back()history.go()history.forward(),或者通過 location.hash = 'xxx' 都會觸發 popstate 事件 和 hashchange 事件
需要注意的是呼叫 history.pushState() 或者 history.replaceState() 不會觸發 popstate 事件 和 hashchange 事件

觸發時機:在 vueRouter 類的 init 方法中呼叫

class VueRouter {
  // router初始化方法(只會在 根vue實體中的 beforeCreate鉤子中呼叫一次)
  init (app) {
    const history = this.history
    // 手動根據當前路徑去匹配對應的組件,渲染,之后監聽路由變化
    history.transitionTo(history.getCurrentLocation(), () => {
      history.setupListener()
    })

    ...
  }
}

push

跳轉頁面,手動呼叫 transitionTo 方法去處理跳轉邏輯,并在回呼中通過 history.pushStatelocation.hash 向路由堆疊添加一條路由記錄,更新地址欄 URL

觸發時機:當我們通過 <router-link> 跳轉路由時會觸發根實體上的 app._router.push() 方法( VueRouter 類中的 push 方法),其內部就呼叫了該方法,即 history 物件(路由歷史實體)的 push 方法,當然,不同的 mode 選項有不同的實作方式

class VueRouter {
  // 呼叫 HashHistory or Html5History 的跳轉邏輯(點擊router-link觸發)
  push (location) {
    // 針對hash模式: history.pushState、不支持再降級為 window.location.hash
    // 針對history模式: history.pushState
    return this.history.push(location)
  }
}

為什么要手動執行 transitionTo 方法?而不是通過 history.pushStatelocation.hash 改變路由堆疊,然后利用 onpopstate 事件 或 onhashchange 事件去間接執行 transitionTo 方法呢 ?

答:因為 history.pushState 不會觸發 onpopstate 事件 ? 這里引出了一個問題,如果我們手動執行了 transitionTo 方法,然后在回呼中用 location.hash 改變路由堆疊,就又會通過 onhashchange 事件再次執行 transitionTo 方法,這里重復執行了 2 遍,所以我們要在 transitionTo 內部做去重處理(后面會詳細介紹其去重邏輯)

Html5History

history 模式,使用 history.pushState/repaceState API 來完成 URL 跳轉,使用onpopstate 事件監聽路由變化,在src/history/html5.js中定義

import Base from './base'

class HTML5History extends Base {
  constructor (router) {
    super(router)
  }

  // 添加監聽器,監聽pathname變化(在 vueRouter類的init方法中呼叫)
  // 當用戶在瀏覽器點擊后退、前進,或者在js中呼叫 history.back(),history.go(),history.forward()等,會觸發popstate事件
  // 但 pushState、replaceState不會觸發這個事件
  setupListener () {
    window.addEventListener('popstate', () => {
      this.transitionTo(window.location.pathname)
    })
  }

  // 獲取pathname  http://192.168.21.144/framework-assets#/assets/1522392838?id=1522392838 => '/framework-assets'
  getCurrentLocation () {
    return window.location.pathname
  }

  // 跳轉頁面
  // 為什么要手動執行 transitionTo,而不是直接改變地址,通過路由監聽器去間接執行 transitionTo?
  // 因為 window.history.pushState() 不會觸發 popstate事件!!!
  push (location) {
    this.transitionTo(location, () => {
      window.history.pushState({}, '', location)
    })
  }
}

export default HTML5History

setupListener

添加路由監聽器,當激活同一檔案中不同的歷史記錄條目時,呼叫 transitionTo 方法統一處理跳轉邏輯,使用了 onpopstate 事件監聽路由變化

觸發時機:在 vueRouter 類的 init 方法中呼叫

呼叫 history.pushState() 或者 history.replaceState() 不會觸發 popstate 事件

push

跳轉頁面,手動呼叫 transitionTo 方法去處理跳轉邏輯,并在回呼中通過 history.pushState 向路由堆疊添加一條路由記錄,更新地址欄 URL

觸發時機:當我們通過 <router-link> 跳轉路由時會觸發根實體上的 app._router.push() 方法( VueRouter 類中的 push 方法),其內部就呼叫了該方法

下一節我們分析一下路由切換到底做了哪些作業

5. 路由切換

當我們點擊<router-link> 進行路由切換時,會通過 push 方法呼叫 base 基類中的 transitionTo 方法處理跳轉邏輯,然后觸發一系列的導航守衛鉤子,如果全部鉤子都執行完了,就會更新根實體上的 _route 回應式屬性,通知 <router-view> 去渲染新的組件

ok!我們具體分析下每一步都是如何實作的

<router-link> 全域組件的注冊是在 VueRouter 類上的 install 方法中,其組件實作是在 src/components/link.js

點擊 <router-link> 時,會呼叫根實體上的 app._router.push() 方法(即 VueRouter 類中的 push 方法),其內部呼叫了 history 物件(路由歷史實體)的 push 方法(即 HashHistory 類或 Html5History 類或中的 push 方法),內部手動呼叫 transitionTo 方法去處理跳轉邏輯,并在回呼中通過 history.pushState 或 location.hash 向路由堆疊添加一條路由記錄,更新地址欄 URL

export default { 
  props: {
    to: { type: String, required: true },
    tag: { type: String, default: 'a' }
  },
  methods: {
    handler () {
      this.$router.push(this.to)
    }
  },
  render () {
    const tag = this.tag
    return <tag style={ { cursor: 'pointer' } } onClick={this.handler}>{this.$slots.default}</tag>
}
}

接下來我們先看下導航守衛,然后一起分析 base 基類中 transitionTo 方法的內部實作

beforeEach

這里,我們只介紹全域前置守衛 beforeEach,用戶可以注冊多個全域前置守衛

const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
  // 回傳 false 以取消導航
  return false
})

router.beforeEach((to, from) => {
  // ...
  // 回傳 false 以取消導航
  return false
})

當一個導航觸發時,全域前置守衛按照創建順序呼叫,守衛是異步決議執行,此時導航在所有守衛 resolve 完之前一直處于等待中,守衛方法接收三個引數

  • to: 即將要進入的目標路由
  • from: 當前導航正要離開的路由
  • next: 進行管道中的下一個守衛鉤子

我們需要用 beforeEachHooks 陣列來收集用戶注冊的守衛鉤子,后續會在 transitionTo 方法中依次執行

class VueRouter {
  constructor (options) {
    this.beforeEachHooks = []
    ...
  }

  // 路由守衛,快取回呼鉤子,在 transitionTo方法中執行回呼鉤子
  beforeEach (cb) {
    this.beforeEachHooks.push(cb)
  }
}

注意!只有當全部鉤子都執行完了,才會去渲染新的路由組件

transitionTo (location, listener) {
	const record = this.router.match(location) // 匹配路由記錄
  const route = createRoute(record, { path: location }) // 生成路由物件
  
  const queue = [].concat(this.router.beforeEachHooks)
  runQueue(queue, this.current, route, () => {
    this.current = route // 更新當前的 current物件, 稍后我們就可以切換頁面顯示
    listener && listener() // 添加路由監聽器 or 更改地址欄url
    this.cb && this.cb(route) // 更新 app._route
  })
}

function runQueue (queue, from, to, cb) {
  function step (index) {
    if (index >= queue.length) return cb()
    const hook = queue[index] // hook就是我們的鉤子方法
    hook(from, to, () => step(index + 1)) // 第三個引數就是 next方法
  }
  step(0)
}

base

base 是 HashHistory 和 Html5History 的基類,主要負責統一處理路由跳轉邏輯,它在 src/history/base.js中定義,讓我們重點來分析下 transitionTo 的內部實作

class Base {
  constructor (router) {
    this.router = router
    this.current = createRoute(null, {
      path: '/'
    })
  }

  // 快取更新_route的回呼(this._route = route)
  listen (cb) {
    this.cb = cb
  }

  // 所有的跳轉邏輯都在這個方法中實作
  // 根據路徑匹配對應的路由記錄,然后更新當前的 current物件 和 app._route物件,
  // 我們之前用 defineReactive 將 app._route 變成了回應式物件,app._route發生變化后,會觸發 setter 劫持,通知 router-view 重新渲染新路徑的組件
  transitionTo (location, listener) {
    // 根據一個路徑匹配對應的路由資訊
    const record = this.router.match(location)

    const route = createRoute(record, { path: location })

    // 去重:當前跳轉的路徑location 和 我們之前存的current.path 相同,而且匹配結果也相同(初始化path:'/'需要額外判斷匹配結果matched),則不再跳轉了
    if (location === this.current.path && route.matched.length === this.current.matched.length) {
      return
    }
    // 如果是 hash模式,并且使用 hashchange監聽路由時,初始化頁面 和 通過route-link跳轉頁面時
    // transitionTo方法執行了兩次(此處列印了兩遍),需要去重處理,當前跳轉的路由location 和 上次的跳轉的路由(v3中實作此屬性)作比較;若一致,則return
    console.log('transitionTo(record)', record, route)

    const queue = [].concat(this.router.beforeEachHooks) // 我們可能有多個鉤子
    runQueue(queue, this.current, route, () => {
      this.current = route // 更新當前的 current物件, 稍后我們就可以切換頁面顯示

      // 添加路由監聽器 or 更改地址欄url
      listener && listener()

      // 更新 app._route
      this.cb && this.cb(route)
    })
  }
}

export default Base

/**
 * @desc 根據樹形結構record路由資訊 回傳一個 扁平化的上下級路由資料
 * 回傳示例:{path:'/', matched:[]}
 * 回傳示例:{path:'/about/a', matched:[aboutRecord, aboutARecord]}
 */
function createRoute (record, location) {
  const matched = []
  if (record) {
    while (record) {
      matched.unshift(record) // [about, about/a]
      record = record.parent
    }
  }
  return {
    ...location,
    matched
  }
}

/**
 * @name 執行路由守衛鉤子
 * @desc 如果有多個beforeEach鉤子,只有在上一個鉤子中執行了next方法,我們才會運行下一個鉤子
 * @desc 只要有一個鉤子未執行next方法,則終止(后續的鉤子、跳轉邏輯均不執行)
 */
function runQueue (queue, from, to, cb) {
  function step (index) {
    if (index >= queue.length) return cb()
    const hook = queue[index] // hook就是我們的鉤子方法
    hook(from, to, () => step(index + 1)) // 第三個引數就是 next方法
  }
  step(0)
}

讓我們根據一個具體場景去進行分析,假如我們要跳轉http://localhost:8080/about/a URL,路由配置如下:

const routes = [
  {
    path: '/about',
    name: 'About',
    component: About,
    children: [
      {
        path: 'a',
        component: {
          render: (h) => <h2>about a</h2>
        }
      },
    ]
  },
  ...
]
  1. 首先會通過 router.match() 根據路徑 /about/a 去匹配對應的路由記錄 record,match 方法是之前由 createMatcher 生成的,record 路由記錄結構如下,此處僅展示部分屬性
{
  "path": "/about/a",
  "component": { about/a 組件定義 },
  "parent": {
    "path": "/about",
    "component": { about 組件定義 }
  }
}
  1. 然后通過 createRoute() 生成一個扁平化的上下級路由資料 route,這就是我們常用的 this.$route 路由物件,route 物件格式如下,此處僅展示部分屬性
{
  "path": "/about/a",
  "matched": [
  	{
  		"path": "/about",
  		"component": { about 組件定義 }
		},
		{
  		"path": "/about/a",
  		"component": { about/a 組件定義 },
  		"parent": {
    			"path": "/about",
    			"component": { about 組件定義 }
  		}
		}
	]	
}
  1. 跳轉一次路由有可能會多次執行 transitionTo 方法,所以我們需要做去重處理,如果當前跳轉的路徑和我們之前快取的相同,則 return
  2. 如果全部導航守衛鉤子都執行完了,則更新當前的 current 物件,并更新根實體上的 _route 回應式物件,然后通知 <router-view> 去渲染新的路由組件,至于<router-view> 是如何去渲染新組件的,我們下一章節再去分析

router-view

<router-view> 全域組件的注冊也是在 VueRouter 類上的 install 方法中,其組件實作是在 src/components/view.js

export default {
  functional: true,
  render (h, { parent, data }) {
    // 默認先渲染 app.vue中的 router-view;再渲染 Home 或 About中的 router-view

    data.routerView = true // 標識該組件是通過 route-view 渲染出來的
    const route = parent.$route // install.js中代理的$route

    let depth = 0
    while (parent) {
      // $vnode 指的是組件本身虛擬DOM
      if (parent.$vnode && parent.$vnode.data.routerView) {
        depth++
      }
      parent = parent.$parent // 不停的向上查找父組件
    }

    // matched是一個包含上下父子路由記錄的陣列,格式如下:[aboutRecord, aboutARecord]
    const record = route.matched[depth]

    // 沒有匹配到組件直接return
    if (!record) {
      return h()
    }

    return h(record.component, data)
  }
}

那么跳轉路由后,<router-view>又是如何知道去渲染新組件的呢?何時渲染?渲染哪一個組件?

先來看第一個問題,何時去渲染?

我們之前用 defineReactive 方法將根實體上的 _route 屬性變成了一個回應式物件,并在 Vue 原型上代理了 $route 屬性,而<router-view> 組件的 render 函式參考了 $route 屬性,所以當 _route 變化后,會觸發 setter 劫持,通知 <router-view> 去自動渲染新的組件

beforeCreate () {
  if (this.$options.router) {
    Vue.util.defineReactive(this, '_route', this._router.history.current)
  }
}

Object.defineProperty(Vue.prototype, '$route', {
  get () {
    return this._routerRoot && this._routerRoot._route
  }
})

第二個問題,他怎么知道渲染哪個組件?

$route 物件的 matched 屬性是一個包含上下父子路由記錄的陣列,在 transitionTo 方法中被創建

讓我們看一個具體的路由配置:

const routes = [
  {
    path: '/about',
    name: 'About',
    component: About,
    children: [
      {
        path: 'a', // children中路徑不能增加 /
        component: {
          render: (h) => <h2>about a</h2>
        }
      },
    ]
  },
	...
]

當我們訪問 http://localhost:8080/about/a URL時, _route 物件(即 this.$route)格式如下:

{
  "path": "/about/a",
	"matched": [
  	{
    	"path": "/about",
    	"component": { about 組件定義 }
  	},
  	{
    	"path": "/about/a",
    	"component": { about/a 組件定義 },
    	"parent": {
      	"path": "/about",
      	"component": { about 組件定義 }
    	}
  	}
	]
}

<router-view> 中 render 函式執行時,我們會不停的向上查找父組件,看是否有 routerView 標識,若存在,則索引深度 depth + 1,然后渲染 h(route.matched[depth].component, data)

組件的渲染是樹狀的,默認先渲染 app.vue 中的 <router-view>h(route.matched[0].component, data),即 About 組件;再渲染 About 中的 <router-view>h(route.matched[1].component, data),即 About/a 組件

6. 參考鏈接

淺顯易懂的vue-router原始碼決議(一)

vue-router 原始碼分析 - 李宇倉 | Li Yucang

珠峰公開課 | vue-router 原始碼

人間不正經生活手冊

轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/556345.html

標籤:其他

上一篇:typescript的必要性及使用

下一篇:返回列表

標籤雲
其他(161881) Python(38266) JavaScript(25517) Java(18284) C(15238) 區塊鏈(8274) C#(7972) AI(7469) 爪哇(7425) MySQL(7273) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5876) 数组(5741) R(5409) Linux(5347) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4609) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2438) ASP.NET(2404) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1985) HtmlCss(1979) 功能(1967) Web開發(1951) C++(1942) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1881) .NETCore(1863) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • IEEE1588PTP在數字化變電站時鐘同步方面的應用

    IEEE1588ptp在數字化變電站時鐘同步方面的應用 京準電子科技官微——ahjzsz 一、電力系統時間同步基本概況 隨著對IEC 61850標準研究的不斷深入,國內外學者提出基于IEC61850通信標準體系建設數字化變電站的發展思路。數字化變電站與常規變電站的顯著區別在于程序層傳統的電流/電壓互 ......

    uj5u.com 2020-09-10 03:51:52 more
  • HTTP request smuggling CL.TE

    CL.TE 簡介 前端通過Content-Length處理請求,通過反向代理或者負載均衡將請求轉發到后端,后端Transfer-Encoding優先級較高,以TE處理請求造成安全問題。 檢測 發送如下資料包 POST / HTTP/1.1 Host: ac391f7e1e9af821806e890 ......

    uj5u.com 2020-09-10 03:52:11 more
  • 網路滲透資料大全單——漏洞庫篇

    網路滲透資料大全單——漏洞庫篇漏洞庫 NVD ——美國國家漏洞庫 →http://nvd.nist.gov/。 CERT ——美國國家應急回應中心 →https://www.us-cert.gov/ OSVDB ——開源漏洞庫 →http://osvdb.org Bugtraq ——賽門鐵克 →ht ......

    uj5u.com 2020-09-10 03:52:15 more
  • 京準講述NTP時鐘服務器應用及原理

    京準講述NTP時鐘服務器應用及原理京準講述NTP時鐘服務器應用及原理 安徽京準電子科技官微——ahjzsz 北斗授時原理 授時是指接識訓通過某種方式獲得本地時間與北斗標準時間的鐘差,然后調整本地時鐘使時差控制在一定的精度范圍內。 衛星導航系統通常由三部分組成:導航授時衛星、地面檢測校正維護系統和用戶 ......

    uj5u.com 2020-09-10 03:52:25 more
  • 利用北斗衛星系統設計NTP網路時間服務器

    利用北斗衛星系統設計NTP網路時間服務器 利用北斗衛星系統設計NTP網路時間服務器 安徽京準電子科技官微——ahjzsz 概述 NTP網路時間服務器是一款支持NTP和SNTP網路時間同步協議,高精度、大容量、高品質的高科技時鐘產品。 NTP網路時間服務器設備采用冗余架構設計,高精度時鐘直接來源于北斗 ......

    uj5u.com 2020-09-10 03:52:35 more
  • 詳細解讀電力系統各種對時方式

    詳細解讀電力系統各種對時方式 詳細解讀電力系統各種對時方式 安徽京準電子科技官微——ahjzsz,更多資料請添加VX 衛星同步時鐘是我京準公司開發研制的應用衛星授時時技術的標準時間顯示和發送的裝置,該裝置以M國全球定位系統(GLOBAL POSITIONING SYSTEM,縮寫為GPS)或者我國北 ......

    uj5u.com 2020-09-10 03:52:45 more
  • 如何保證外包團隊接入企業內網安全

    不管企業規模的大小,只要企業想省錢,那么企業的某些服務就一定會采用外包的形式,然而看似美好又經濟的策略,其實也有不好的一面。下面我通過安全的角度來聊聊使用外包團的安全隱患問題。 先看看什么服務會使用外包的,最常見的就是話務/客服這種需要大量重復性、無技術性的服務,或者是一些銷售外包、特殊的職能外包等 ......

    uj5u.com 2020-09-10 03:52:57 more
  • PHP漏洞之【整型數字型SQL注入】

    0x01 什么是SQL注入 SQL是一種注入攻擊,通過前端帶入后端資料庫進行惡意的SQL陳述句查詢。 0x02 SQL整型注入原理 SQL注入一般發生在動態網站URL地址里,當然也會發生在其它地發,如登錄框等等也會存在注入,只要是和資料庫打交道的地方都有可能存在。 如這里http://192.168. ......

    uj5u.com 2020-09-10 03:55:40 more
  • [GXYCTF2019]禁止套娃

    git泄露獲取原始碼 使用GET傳參,引數為exp 經過三層過濾執行 第一層過濾偽協議,第二層過濾帶引數的函式,第三層過濾一些函式 preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'] (?R)參考當前正則運算式,相當于匹配函式里的引數 因此傳遞 ......

    uj5u.com 2020-09-10 03:56:07 more
  • 等保2.0實施流程

    流程 結論 ......

    uj5u.com 2020-09-10 03:56:16 more
最新发布
  • Vue Router 原始碼分析

    最終成果,實作了一個可運行的核心路由工程:柏成/vue-router3.x。地址如下:https://gitee.com/lbcjs/vue-router3.x ......

    uj5u.com 2023-06-30 08:22:12 more
  • typescript的必要性及使用

    作為一個前端語言,Javascript從最初只是用來寫頁面,到如今的移動終端、后端服務、神經網路等等,它變得幾乎無處不在。如此廣闊的應用領域,對語言的安全性、健壯性以及可維護性都有了更高的要求。盡管ECMAScript標準在近幾年有了長足的進步,但是在型別檢查方面依然毫無建樹。在這種情況下TypeS... ......

    uj5u.com 2023-06-30 08:21:59 more
  • CSS基礎-背景

    # 背景 ### **background-color** 背景顏色, 可以使用十六進制、rgb、rgba表示。 **語法** ```css /**selector 背景元素的原則去*/ /** color 背景顏色的值, 可以是 顏色名稱、十六進制值、RGB、RGBA*/ selector { b ......

    uj5u.com 2023-06-30 08:20:28 more
  • 京東到家小程式-在性能及多端能力的探索實踐

    為了提高研發效率,經過技術選型采用了taro3+原生混合開發模式,本文主要講解我們是如何基于taro框架,進行多端能力的探索和性能優化。 ......

    uj5u.com 2023-06-30 08:20:15 more
  • 初入前端-HTML

    ## HTML ### HTML歷史 HTML(Hypertext Markup Language)的歷史可以追溯到上世紀90年代初,以下是HTML的主要歷史階段: 1. HTML 1.0:在1991年發布,是HTML的最初版本,用于創建基本的文本和鏈接結構,但功能有限。 2. HTML 2.0:于 ......

    uj5u.com 2023-06-30 08:20:10 more
  • 圖書商城Vue+Element+Node專案練習(...)

    本系列文章是為學習Vue的專案練習筆記,盡量詳細記錄一下一個完整專案的開發程序。面向初學者,本人也是初學者,搬磚技識訓不成熟。專案在技術上前端為主,包含一些后端代碼,從基礎的資料庫(Sqlite)、到后端服務Node.js(Express),再到Web端的Vue,包含服務端、管理后臺、商城網站、小程... ......

    uj5u.com 2023-06-29 08:54:51 more
  • 記錄--不定高度展開收起影片 css/js 實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 不定高度展開收起影片 最近在做需求的時候,遇見了元素高度展開收起的影片需求,一開始是想到了使用 transition: all .3s; 來做影片效果,在固定高度的情況下,transition 影片很好使,滿足了需求,但是如果要考慮之后可 ......

    uj5u.com 2023-06-29 08:54:35 more
  • 前端打包部署后介面BASE_URL不對問題解決辦法

    在前端打包部署時,為了免去不同環境打包的麻煩,專案用的流水線觸發方式。在這里不細說,重點說說下面情況。 當專案提交打包部署后,訪問壓測環境或者生產環境的地址來使用專案時,發現介面報錯404。 在NETWORK里發現介面的BASEURL和當前環境需要呼叫的后端baseurl不同。 主要問題在于配置問題 ......

    uj5u.com 2023-06-29 08:54:22 more
  • this指向性問題

    this的查找規則會逐層往上查找,最終位全域window 優先級問題:顯式系結(顯式系結與new系結沒有可比性)new系結>隱式系結>默認系結 在編程中,this 是一個關鍵字,代表當前物件或者函式的執行環境。this 的指向性問題是指在不同的情況下,this 指向的物件不同,從而影響代碼的行為。 ......

    uj5u.com 2023-06-29 08:54:12 more
  • this指向性問題

    this的查找規則會逐層往上查找,最終位全域window 優先級問題:顯式系結(顯式系結與new系結沒有可比性)new系結>隱式系結>默認系結 在編程中,this 是一個關鍵字,代表當前物件或者函式的執行環境。this 的指向性問題是指在不同的情況下,this 指向的物件不同,從而影響代碼的行為。 ......

    uj5u.com 2023-06-29 08:48:10 more