專欄分享: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 件事 !
-
我們利用
Vue.mixin將 beforeCreate 生命周期鉤子注入到了每一個組件中,并在組件自身鉤子之前呼叫在 beforeCreate 鉤子中,我們將根實體
_routerRoot共享給了所有的后代組件;給根實體添加了一個_router屬性(VueRouter實體)并呼叫了 router 的初始化方法this._router.init();然后用defineReactive方法給根實體添加了一個回應式屬性_route(當前路由物件) -
在 Vue 原型上代理了
$router、$route屬性,這就是為什么我們可以在任何組件內通過this.$router訪問路由器、通過this.$route訪問當前路由 -
通過
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 初始化方法中主要做了兩件事!
- 執行 history.transitionTo 方法,根據當前路徑去匹配對應的組件,并渲染,然后通過
history.setupListener在回呼中添加路由監聽器,當路由變化時,我們就可以做一些事情了!當然,不同的路由模式有不同的 setupListener 實作(后面會詳細介紹)
history.transitionTo(history.getCurrentLocation(), () => {
history.setupListener()
})
- 執行 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.pushState 或 location.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.pushState 或 location.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
<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>
}
},
]
},
...
]
- 首先會通過
router.match()根據路徑/about/a去匹配對應的路由記錄 record,match 方法是之前由 createMatcher 生成的,record 路由記錄結構如下,此處僅展示部分屬性
{
"path": "/about/a",
"component": { about/a 組件定義 },
"parent": {
"path": "/about",
"component": { about 組件定義 }
}
}
- 然后通過
createRoute()生成一個扁平化的上下級路由資料 route,這就是我們常用的this.$route路由物件,route 物件格式如下,此處僅展示部分屬性
{
"path": "/about/a",
"matched": [
{
"path": "/about",
"component": { about 組件定義 }
},
{
"path": "/about/a",
"component": { about/a 組件定義 },
"parent": {
"path": "/about",
"component": { about 組件定義 }
}
}
]
}
- 跳轉一次路由有可能會多次執行 transitionTo 方法,所以我們需要做去重處理,如果當前跳轉的路徑和我們之前快取的相同,則 return
- 如果全部導航守衛鉤子都執行完了,則更新當前的 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
標籤:其他
下一篇:返回列表
