淺析vue-router實現(xiàn)原理及兩種模式
之前用Vue開發(fā)單頁應用,發(fā)現(xiàn)不管路由怎么變化,瀏覽器地址欄總是會有一個'#'號。

當時檢查自己的代碼,沒有發(fā)現(xiàn)請求的地址帶'#',當時也很納悶,但是由于沒有影響頁面的渲染以及向后臺發(fā)送請求,當時也沒有在意。最近看了一下vue-router的實現(xiàn)原理,才逐漸揭開了這個謎題。
vue-router 的兩種方式(瀏覽器環(huán)境下)
1. Hash (對應HashHistory)
hash(“#”)符號的本來作用是加在URL中指示網(wǎng)頁中的位置:
http://www.example.com/index.html#print
#符號本身以及它后面的字符稱之為hash(也就是我之前為什么地址欄都會有一個‘#'),可通過window.location.hash屬性讀取。它具有如下特點:
hash雖然出現(xiàn)在URL中,但不會被包括在HTTP請求中。它是用來指導瀏覽器動作的,對服務器端完全無用,因此,改變hash不會重新加載頁面
2.可以為hash的改變添加監(jiān)聽事件:
window.addEventListener("hashchange", funcRef, false)
每一次改變hash(window.location.hash),都會在瀏覽器的訪問歷史中增加一個記錄
利用hash的以上特點,就可以來實現(xiàn)前端路由“更新視圖但不重新請求頁面”的功能了。
2. History (對應HTML5History)
History接口 是瀏覽器歷史記錄棧提供的接口,通過back(), forward(), go()等方法,我們可以讀取瀏覽器歷史記錄棧的信息,進行各種跳轉操作。
從HTML5開始,History interface提供了兩個新的方法:pushState(), replaceState()使得我們可以對瀏覽器歷史記錄棧進行修改:
window.history.pushState(stateObject, title, URL) window.history.replaceState(stateObject, title, URL)
stateObject: 當瀏覽器跳轉到新的狀態(tài)時,將觸發(fā)popState事件,該事件將攜帶這個stateObject參數(shù)的副本 title: 所添加記錄的標題 URL: 所添加記錄的URL
這兩個方法有個共同的特點:當調用他們修改瀏覽器歷史記錄棧后,雖然當前URL改變了,但瀏覽器不會刷新頁面,這就為單頁應用前端路由“更新視圖但不重新請求頁面”提供了基礎。 瀏覽器歷史記錄可以看作一個「?!?。棧是一種后進先出的結構,可以把它想象成一摞盤子,用戶每點開一個新網(wǎng)頁,都會在上面加一個新盤子,叫「入?!?。用戶每次點擊「后退」按鈕都會取走最上面的那個盤子,叫做「出?!埂6看螢g覽器顯示的自然是最頂端的盤子的內(nèi)容。
vue-router 的作用
vue-router的作用就是通過改變URL,在不重新請求頁面的情況下,更新頁面視圖。簡單的說就是,雖然地址欄的地址改變了,但是并不是一個全新的頁面,而是之前的頁面某些部分進行了修改。
export default new Router({
// mode: 'history', //后端支持可開
routes: constantRouterMap
})
這是Vue項目中常見的一段初始化vue-router的代碼,之前沒仔細研究過vue-router,不知道還有一個mode屬性,后來看了相關文章后了解到,mode屬性用來指定vue-router使用哪一種模式。在沒有指定mode的值,則使用hash模式。
源碼分析
首先看一下vue-router的構造函數(shù)
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'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract': //非瀏覽器環(huán)境下
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
主要是先獲取mode的值,如果mode的值為 history 但是瀏覽器不支持 history 模式,那么就強制設置mode值為 hash 。如果支持則為 history 。接下來,根據(jù)mode的值,來選擇vue-router使用哪種模式。
case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break
這樣就有了兩種模式。確定好了vue-router使用哪種模式后,就到了init。 先來看看router 的 init 方法就干了哪些事情,在 src/index.js 中
init (app: any /* Vue component instance */) {
// ....
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 => {
this.apps.forEach((app) => {
app._route = route
})
})
}
// ....
// VueRouter類暴露的以下方法實際是調用具體history對象的方法
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)
}
}
如果是HTML5History,則執(zhí)行
history.transitionTo(history.getCurrentLocation())
如果是Hash模式,則執(zhí)行
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
可以看出,兩種模式都執(zhí)行了transitionTo( )函數(shù)。 接下來看一下兩種模式分別是怎么執(zhí)行的,首先看一下Hash模式
HashHistory.push()
我們來看HashHistory中的push()方法:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
pushHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function pushHash (path) {
window.location.hash = path
}
transitionTo()方法是父類中定義的是用來處理路由變化中的基礎邏輯的,push()方法最主要的是對window的hash進行了直接賦值:
window.location.hash = route.fullPath hash的改變會自動添加到瀏覽器的訪問歷史記錄中。
那么視圖的更新是怎么實現(xiàn)的呢,我們來看父類History中transitionTo()方法的這么一段:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// 調用 match 得到匹配的 route 對象
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
this.updateRoute(route)
...
})
}
updateRoute (route: Route) {
this.cb && this.cb(route)
}
listen (cb: Function) {
this.cb = cb
}
可以看到,當路由變化時,調用了History中的this.cb方法,而this.cb方法是通過History.listen(cb)進行設置的?;氐絍ueRouter類定義中,找到了在init()方法中對其進行了設置:
init (app: any /* Vue component instance */) {
this.apps.push(app)
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
代碼中的app指的是Vue的實例,._route本不是本身的組件中定義的內(nèi)置屬性,而是在Vue.use(Router)加載vue-router插件的時候,通過Vue.mixin()方法,全局注冊一個混合,影響注冊之后所有創(chuàng)建的每個 Vue 實例,該混合在beforeCreate鉤子中通過Vue.util.defineReactive()定義了響應式的_route。所謂響應式屬性,即當_route值改變時,會自動調用Vue實例的render()方法,更新視圖。vm.render()是根據(jù)當前的 _route 的path,name等屬性,來將路由對應的組件渲染到. 所以總結下來,從路由改變到視圖的更新流程如下:
this.$router.push(path) --> HashHistory.push() --> History.transitionTo() --> const route = this.router.match(location, this.current)會進行地址匹配,得到一個對應當前地址的route(路由信息對象) --> History.updateRoute(route) --> app._route=route (Vue實例的_route改變) 由于_route屬性是采用vue的數(shù)據(jù)劫持,當_route的值改變時,會執(zhí)行響應的render( ) -- > vm.render() 具體是在<router-view></router-view> 中render --> window.location.hash = route.fullpath (瀏覽器地址欄顯示新的路由的path)
HashHistory.replace()
說完了HashHistory.push(),該說HashHistory.replace()了。
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
replaceHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
可以看出來,HashHistory.replace它與push()的實現(xiàn)結構上基本相似,不同點在于它不是直接對window.location.hash進行賦值,而是調用window.location.replace方法將路由進行替換。這樣不會將新路由添加到瀏覽器訪問歷史的棧頂,而是替換掉當前的路由。
監(jiān)聽地址欄
可以看出來,上面的過程都是在代碼內(nèi)部進行路由的改變的,比如項目中常見的this.$router.push(), 等方法。然后將瀏覽器的地址欄置為新的hash值。那么如果直接在地址欄中輸入URL從而改變路由呢,例如

我將dashboadr刪除,然后置為article/hotSpot,然后回車,vue又是如何處理的呢?
setupListeners () {
window.addEventListener('hashchange', () => {
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
replaceHash(route.fullPath)
})
})
}
該方法設置監(jiān)聽了瀏覽器事件hashchange,調用的函數(shù)為replaceHash,即在瀏覽器地址欄中直接輸入路由相當于代碼調用了replace()方法.后面的步驟自然與HashHistory.replace()相同,一樣實現(xiàn)頁面渲染。
HTML5History
HTML5History模式的vue-router 代碼結構以及更新視圖的邏輯與hash模式基本類似,和HashHistory的步驟基本一致,只是HashHistory的push和replace()變成了HTML5History.pushState()和HTML5History.replaceState()
在HTML5History中添加對修改瀏覽器地址欄URL的監(jiān)聽是直接在構造函數(shù)中執(zhí)行的,對HTML5History的popstate 事件進行監(jiān)聽:
constructor (router: Router, base: ?string) {
window.addEventListener('popstate', e => {
const current = this.current
this.transitionTo(getLocation(this.base), route => {
if (expectScroll) {
handleScroll(router, route, current, true)
}
})
})
}
以上就是vue-router hash模式與history模式不同模式下處理邏輯的分析了。
總結
以上所述是小編給大家介紹的vue-router實現(xiàn)原理及兩種模式分析,希望對大家有所幫助!
相關文章
解決axios post 后端無法接收數(shù)據(jù)的問題
今天小編就為大家分享一篇解決axios post 后端無法接收數(shù)據(jù)的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-10-10
vue數(shù)據(jù)監(jiān)聽解析Object.defineProperty與Proxy區(qū)別
這篇文章主要為大家介紹了vue數(shù)據(jù)監(jiān)聽解析Object.defineProperty Proxy源碼示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03
Spring Boot/VUE中路由傳遞參數(shù)的實現(xiàn)代碼
在路由時傳遞參數(shù),一般有兩種形式,一種是拼接在url地址中,另一種是查詢參數(shù)。這篇文章主要介紹了Spring Boot/VUE中路由傳遞參數(shù),需要的朋友可以參考下2018-03-03
vue.js 圖片上傳并預覽及圖片更換功能的實現(xiàn)代碼
這篇文章主要介紹了vue.js 圖片上傳并預覽及圖片更換功能,小編主要圍繞我們?nèi)粘J褂霉δ艿睦幼鲋v解,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2018-08-08
Vue中computed屬性和watch,methods的區(qū)別
本文主要介紹了Vue中computed屬性和watch,methods的區(qū)別,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-05-05
如何解決element-ui動態(tài)加載級聯(lián)選擇器默認選中問題
這篇文章主要介紹了如何解決element-ui動態(tài)加載級聯(lián)選擇器默認選中問題,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的朋友可以參考一下2022-09-09
Vue中使用vue2-perfect-scrollbar制作滾動條
這篇文章主要介紹了Vue中使用vue2-perfect-scrollbar滾動條,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-06-06
Vue?3?表格時間監(jiān)控與動態(tài)后端請求觸發(fā)詳解?附Demo展示
在Vue3中,使用el-table組件渲染表格數(shù)據(jù),通過el-table-column指定內(nèi)容,時間點需前端校準,用getTime()比較,到達時觸發(fā)操作,異步API請求可用async/await處理,setInterval實現(xiàn)定時監(jiān)控,配合條件判斷防止重復請求2024-09-09

