關於vue-router
實現原理的問題是很是重要的,而且常常會在面試中提問
本章簡單講解一下 vue-router
中 router-link
、router-view
、$router
、$route
的實現原理html
裏面的註釋可能會有點多,可是仍是本着走一步測一步的原則,慢慢看,慢慢來前端
說到前端路由,不得不說路由的兩種模式:vue
兩種路由的具體區別和使用,請參考個人文章:《Vue的mode中 hash 與 history 的區別》git
hash模式的特性:面試
URL
中 hash
值只是客戶端的一種狀態,也就是說當向服務器端發出請求時,hash
部分不會被髮送。hash
值的改變,都會在瀏覽器的訪問歷史中增長一個記錄。所以咱們能經過瀏覽器的回退、前進按鈕控制 hash
的切換。hashchange
事件來監聽 hash
的變化。咱們能夠經過兩種方式觸發 hash
變化,一種是經過 a
標籤,並設置 href
屬性,當用戶點擊這個標籤後,URL
就會發生改變,也就會觸發 hashchange
事件
還有一種方式就是直接使用 JS 來對 location.hash
進行賦值,從而改變 URL
,觸發 hashchange
事件vue-router
Hash實現原理api
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hash原理</title>
</head>
<body>
<!-- hash原理:靠的是location.hash load事件 hashchange事件 -->
<a href="#/home">首頁</a>
<a href="#/about">關於</a>
<div id="box"></div>
</body>
<script>
// 當Html文檔加載完畢後,會觸發load事件
window.addEventListener("load",()=>{
// 在瀏覽器中有一個api叫location
// console.log(location.hash.slice(1))
// location是瀏覽器自帶 也就是說是Hash路由的原理是location.hash
box.innerHTML = location.hash.slice(1)
})
// hashchange 當hash改變時觸發這個事件
window.addEventListener("hashchange",()=>{
box.innerHTML = location.hash.slice(1)
})
</script>
</html>
複製代碼
history模式的特性:數組
pushState
和 repalceState
的標題(title):通常瀏覽器會忽略,最好傳入 null
;popstate
事件來監聽 URL
的變化;history.pushState()
或 history.replaceState()
不會觸發 popstate
事件,這時咱們須要手動觸發頁面渲染;History實現原理瀏覽器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>history原理</title>
</head>
<body>
<!-- history原理:靠的是h5中的api:history.pushState popstate 等 -->
<a onclick="go('/home')">首頁</a>
<a onclick="go('/about')">關於</a>
<div id="box"></div>
</body>
<script>
function go(pathname){
//pushState方法,裏面有三個參數
history.pushState({},null,pathname)
box.innerHTML = pathname
}
//popstate方法
window.addEventListener("popstate",()=>{
// console.log(".....")
console.log(location.pathname)
go(location.pathname)
})
</script>
</html>
複製代碼
爲了使代碼更加的優雅,咱們在 src
目錄下新建了一個文件夾 router
,用來代替外面的 router.js
文件,而且在 router
文件夾下新建了index.js
、routes.js
、vue-router.js
文件。bash
總體結構如圖所示:
在上圖中,咱們不使用Vue框架中的router
,由於人家的router
裏已經給咱們封裝好了router-view
、router-link
、$router
、$route
等一系列方法,因此咱們使用本身寫的代碼。
其中routes.js
中定義了咱們須要用到的路由,而vue-router.js
文件中則至關於vue-router
的源碼。
組件Home.vue
和About.vue
很簡單,就打印一個內容,以下:
Home.vue
<template>
<div>
Home
</div>
</template>
<script>
export default {
name:'home'
}
</script>
複製代碼
About.vue
<template>
<div>
About
</div>
</template>
<script>
export default {
name:'about'
}
</script>
複製代碼
而後在咱們建立的router
文件夾下的routes.js
文件中引入上面的兩個路由:
import Home from '../views/Home.vue'
import About from '../views/About.vue'
export default [
{path:'/home',component:Home},
{path:'/about',component:About},
]
複製代碼
而後在咱們的index.js
文件中引入routes.js
文件,而且把咱們將要寫Vue源碼的文件vue-router.js
文件也引入到裏面
import Vue from 'vue'
import VueRouter from './vue-router'
import routes from './routes'
//一旦使用Vue.use就會調用install方法
Vue.use(VueRouter)
export default new VueRouter({
mode:'history',
routes
})
複製代碼
接下來開始在vue-router
中寫源碼
首先咱們定義一個VueRouter
類,並在裏面建立一個constructor
,並把這個類導出去:
//VueRouter就是一個類
class VueRouter{
constructor(options){ // constructor指向建立這個對象的函數 下面定義的這些變量都將掛在VueRouter實例上
//打印options以下
console.log(options); //{mode: "hash", routes: Array(2)}
// 獲得路由模式
this.mode=options.mode || "hash"
// 實例化HistoryRoute掛在VueRouter組件上,this指向VueRouter這個類
this.history=new HistoryRoute()
// 獲得路由規則
this.routes=options.routes || []
// 轉換後的數據形式 {'/home':Home}
this.routesMap=this.createMap(this.routes)
this.init() //刷新頁面就會調用init方法
}
}
// 把類導出去
export default VueRouter
複製代碼
咱們須要把路由格式轉換成咱們須要的那種格式,就如上面代碼所示,獲得路由規則後,而後把他傳入createMap
中,開始轉換格式並返回賦值給routesMap
// 在VueRouter中定義一個方法,把數組形式的路由轉變成對象形式,即[{path:'/home',component:'Home'}]轉變成{'/home':Home},這樣寫,下面根據路徑渲染組件時較爲簡潔,方便
createMap(routes){
// array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
// total--初始值,或者計算結束後返回的值,當前值,當前元素的索引(可選),當前元素所屬數組(可選),initialValue可選,傳遞給函數的初值
return routes.reduce((memo,current)=>{
// console.log(memo)
memo[current.path]=current.component
return memo
},{})
}
複製代碼
接下來在VueRouter
中定義init()
方法,判斷當前使用的是什麼路由,而且把獲得的路徑保存到current
中 :
先在VueRouter
類的外面(和VueRouter
同級)寫一個HistoryRoute
類用來保存獲得的路徑:
class HistoryRoute{
constructor(){
this.current = null
}
}
複製代碼
接下來調用init()
方法把得到到的路徑保存到current
裏,以下:
init(){
// hash的原理是location,location.hash表示訪問的路徑,還有兩個事件,load,hashchange
//使用的是hash路由
if(this.mode==="hash"){
// location是瀏覽器內部的一個api
// console.log(location.hash) // #/ #/home #/about
location.hash ? "" : location.hash="/"
// 當頁面加載完畢後觸發load事件
window.addEventListener("load",()=>{
// console.log(location.hash.slice(1)) // / /home /about
// current保存了響應的路徑
this.history.current = location.hash.slice(1) // 去掉#是爲了下面匹配{path:'/home'}時能夠匹配到
// console.log(this.history.current) // / /home /about
})
// 點擊前進後退按鈕時纔會hashchange事件
window.addEventListener("hashchange",()=>{
this.history.current=location.hash.slice(1)
// console.log(this.history.current)
})
}
// history模式,靠的是popstate,location.pathname表示訪問的路徑
else
{
//使用的是history
location.pathname ? "" : location.pathname = "/"
// console.log(location.pathname) // / /home /about
// 頁面加載完畢後觸發load事件
window.addEventListener("load",()=>{
this.history.current=location.pathname
console.log("----",this.history.current)
})
// 當用戶點擊回退或前進按鈕時纔會觸發popstate事件
window.addEventListener("popstate",()=>{
this.history.current=location.pathname
console.log(this.history.current)
})
}
}
複製代碼
如今在VueRouter
裏定義一些方法,以下:
push(){}
go(){}
back(){}
複製代碼
重點來了:
給VueRouter
上面掛載一個install
方法,並讓所有的組件均可以使用$router
和$route
:
//install方法中第一個參數就是Vue構造器
VueRouter.install = function(Vue){
// console.log(Vue); //Vue構造器
//當使用Vue.use(Vue-router)時,調用install方法
//Vue.component() //全局組件
Vue.mixin({
//給每一個組件混入一個beforeCreate鉤子函數,當訪問根組件時觸發
beforeCreate(){
// console.log(this.$options.name);
//獲取根組件
if(this.$options && this.$options.router){
//找到根組件
//把當前的實例掛載到_root上面
this._root = this //main根組件
//把router實例掛載到_router上
this._router = this.$options.router
//監控路徑變化,路徑變化就刷新視圖
Vue.util.defineReactive(this,'xxx',this._router,history) //這行代碼能夠給咱們的history設置get和set,使history變成響應式
}
else
{
//全部組件中都是有router
this._root = this.$parent._root;
this._router = this.$parent._router;
}
// this.$options.name 獲取組件的名字
// console.log(this.$options.name)
//讓$router在全局中可用
// defineProperty(obj,prop,descriptor),
// obj:要定義的屬性的對象;prop:要定義或修改的對象的屬性;descriptor:要定義或修改的屬性值
Object.defineProperty(this,"$router",{ // this表示每個組件
get(){ // 獲取$router時,調用get,返回this._root._router,相似的還有一個set方法,設置$router時調用set方法
return this._root._router;
}
})
//讓$route在全局中可用
Object.defineProperty(this,"$route",{
get(){ // 訪問$route時,自動調用get方法
// console.log(this._root._router.history);
return{
current:this._root._router.history.current
}
}
})
}
})
//讓router-link在全局中可用
Vue.component('router-link',{
props: {
to:String
},
render(h){
let mode = this._self._root._router.mode;
return <a href={mode==='hash'?`#${this.to}`:this.to}>{this.$slots.default}</a>
}
})
//讓router-view在全局中可用
Vue.component('router-view',{
render(h){
let current = this._self._root._router.history.current;
let routesMap = this._self._root._router.routesMap;
//current是一個變量,因此用[]包起來用
return h(routesMap[current])
}
})
}
複製代碼
接下來在App.vue
中就可使用了:
<template>
<div>
<router-link to="/home">首頁</router-link>
<br>
<router-link to="/about">關於</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
name:'app',
mounted(){
//下面兩個就能夠打印出相應的內容了
// console.log(this.$router);
// console.log(this.$route);
}
}
</script>
複製代碼
若是隻看代碼,沒有本身實踐一番,確定是理解不了的
在此處附上源碼--->源碼地址