理解vue-router中(router-link router-view $router $route)實現原理

關於vue-router實現原理的問題是很是重要的,而且常常會在面試中提問
本章簡單講解一下 vue-routerrouter-linkrouter-view$router$route 的實現原理html

裏面的註釋可能會有點多,可是仍是本着走一步測一步的原則,慢慢看,慢慢來前端

路由模式

說到前端路由,不得不說路由的兩種模式:vue

  • Hash 模式
  • History 模式

兩種路由的具體區別和使用,請參考個人文章:《Vue的mode中 hash 與 history 的區別》git

Hash模式

hash模式的特性:面試

  • URLhash 值只是客戶端的一種狀態,也就是說當向服務器端發出請求時,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模式

history模式的特性:數組

  • pushStaterepalceState 的標題(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.jsroutes.jsvue-router.js文件。bash

總體結構如圖所示:

圖片加載失敗!

在上圖中,咱們不使用Vue框架中的router,由於人家的router裏已經給咱們封裝好了router-viewrouter-link$router$route等一系列方法,因此咱們使用本身寫的代碼。
其中routes.js中定義了咱們須要用到的路由,而vue-router.js文件中則至關於vue-router的源碼。

組件Home.vueAbout.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>
複製代碼

源碼

若是隻看代碼,沒有本身實踐一番,確定是理解不了的

在此處附上源碼--->源碼地址


^_<

相關文章
相關標籤/搜索