Vue-Router核心原理實現(文末附手寫版源碼)

VueRouter的核心原理

一、VueRouter的核心組成部分

  • 主要實現以下幾部分:
    • mode
    • this.$router/this.$route
    • router-link/router-view
    • Vue.use註冊插件

1、mode

在vueRouter中,主要分爲兩種模式,一種是hash,一種是history。

  • hash模式是通過監聽hashchange事件,然後根據hash值去加載對應的內容的。
  • history模式是通過history.pushState來添加瀏覽器歷史記錄,然後通過監聽popState事件,也就是監聽歷史記錄的改變,來加載相應的內容。
    • 當然,這種模式會有問題,可能我刷新了下瀏覽器,這個路由是不存在的,就會發生404的情況。
    • 舉例:如下代碼,點擊關於兩個字,內容顯示/about,地址欄路徑變成了/about,這時候去刷新頁面,發現找不到/about路徑對應的內容,就出現了404,Not Found
	<a onclick="go('/about')">關於</a>
	<div id="html">
	</div>
	function go(pathname){
		// 通過history模式,頁面不會刷新,只會往歷史記錄中增加歷史記錄
		history.pushState({}, null, pathname);//第一個參數是傳入的數據。第二個是名字,無實際意義。第三個是路徑
		html.innerHTML = pathname;
	}
	// 監聽popstate事件,也就是監聽歷史記錄改變,比如前進或後退,就會觸發該事件
	window.addEventListener('popstate', () => {
		html.innerHTML = location.pathname;
	})

2、this.$router/this.$route

  • this.$router其實就是vueRouter這個實例,也就是我們掛載在根實例options上的router,我們常常會去使用this.$router上的一些方法。
	new Vue({
		el:'#app',
		router,
		render:(h) => h(App)
	})
  • this.$route其實保存了一些屬性,比如當前路徑current等。
  • 當然,除了這些,我們也會遇到一丁點問題,比如如何讓所有組件都能訪問到根組件上掛在的router實例?——Vue.mixin+beforeCreate+遞歸(類似)來解決。

3、router-link/router-view

我們在vue中,經常用標籤的模式去使用router-link/router-view,可見,router-link/router-view其實是兩個組件。

  • router-link其實是一個a標籤,當然,也可以通過tag屬性傳入的方式自定義標籤。另外,還有to屬性,代表路由跳轉的去向。
  • router-view,渲染當前路徑所對應的組件內容。那麼如何渲染,只要找到路徑對應的組件,然後render函數來渲染即可。當然,在我們真正寫原理的時候,會發現,除了這些,還有bug讓我們去解決——先註冊vue組件,再去加載頁面,執行router-view的render函數時,根本獲取不到當前路徑,怎麼辦?深度劫持幫大忙。

4、Vue.use註冊插件

  • 當我們在使用VueRouter的時候,常常是通過Vue.use()方法來註冊插件,其實內部是調用了VueRouter上的install方法。

二、源碼實現

  • 直接上源碼,主要看註釋
class HistoryRoute{
    constructor(){
        this.current = null;
    }
}


class VueRouter{
    constructor(options){
        this.mode = options.mode || 'hash';
        this.routes = options.routes || [];
        // 你傳遞的路由表是一個數組,改造成一個對象:key是路徑,value是路徑對應的組件{'/home':Home,'/about':About}
        this.routesMap = this.createMap(this.routes);
        // 路由中需要存放當前的路徑,當前路徑變化的時候,應該渲染對應的組建
        // this.history = {current:null};// 默認是null  當然,我們最好用一個類來管理當前路徑,這樣方便拓展
        this.history = new HistoryRoute();
        this.init(); // 開始初始化操作
    }
    init(){
        // 判斷是什麼模式,如果是hash應該怎麼樣,如果是history應該怎麼樣
        // 如果是hash
        if(this.mode === 'hash'){
            // 先判斷用戶打開時有沒有hash,沒有就跳轉到#/這樣的路徑
            location.hash ? '' : location.hash = '/';
            // 讀取當前的路徑,比如是/home,存到this.history中
            //剛加載頁面的時候,把路徑放到this.history中
            window.addEventListener('load', () => {
                this.history.current = location.hash.slice(1);
            });
            //當路由改變的時候,也要將路徑存放到this.history中
            window.addEventListener('hashchange', () => {
                this.history.current = location.hash.slice(1);
            })

        }else{// 如果是history
            // 先判斷用戶打開的時候有沒有路徑,沒有的惡化就跳轉到#/這樣的路徑
            location.pathname ? '' : location.path = '/';
            // 頁面一加載就把路徑放到this.histroy中,這個跟hash是一樣的,唯一區別的是用pathname,不是hash而已
            window.addEventListener('load', () => {
                this.history.current = location.pathname;
            });
            //當瀏覽器前進後退的時候,要將對應的路徑放到this.history中
            window.addEventListener('popstate', () => {
                this.history.current = location.pathname;
            })
        }
        
    }
    createMap(routes){
        return routes.reduce((prev, next, index, arr) => {
            prev[next.path] = next.component;
            return prev;
        },{})
    }
}

// 使用Vue.use,會自動調用插件這個類的install方法
VueRouter.install = function(Vue, options){
    console.log(Vue, 'install');
    // 每個組件都有this.$router / this.$route
    // 如何使得每個組建都有this.$router/this.$route,那就要用到Vue.minxin
    Vue.mixin({
        beforeCreate(){
            // 獲取組件的屬性名稱

            if(this.$options && this.$options.router){// 說明這是根組件
                this._root = this;//把當前實例掛在在_root上
                this._router = this.$options.router;// 把router實例掛在在_router上
                //observer方法
				//如果history中的current屬性變化,也會刷新視圖
				//this.xxx = this._router.histoury
				Vue.util.defineReactive(this,'xxx',this._router.history)//深度劫持,去看MVVM的原理
            }else{
                // vue組件的渲染順序 父->子->孫
                this._root = this.$parent._root;
            }


            Object.defineProperty(this, '$router', {
                get(){
                    return this._root._router;
                }
            });
            Object.defineProperty(this, '$route', {
                get(){
                    return {
                        //當前路由所在的值
                        current:this._root._router.history.current
                    };
                }
            });
        }
    });
    Vue.component('router-link',{
        props:{
			to:String,
			tag:String
		},
		methods:{
			handleClick(){
				let mode = this._self._root._router.mode;
				if(mode === 'hash'){
					location.hash = this.to;
				}else{
					location.pathname = this.to;
				}
			}
		},
		render(h){//這個h是createElement
			//這裏的this是什麼?在render方法中,this是Proxy,如果你要拿到Vue實例(組件),那麼就要用this._self

			//要先取到模式,然後根據模式來對應返回
			let mode = this._self._root._router.mode;
			let tag = this.tag ? this.tag : 'a';
			return <tag on-click={this.handleClick} href={mode === 'hash' ? `#${this.to}` : this.to}>{this.$slots.default}</tag>;
		}
    })
    Vue.component('router-view',{// 根據當前的狀態 current 路由表{'/about':About}
        render(h){
            //這裏的this是什麼?在render方法中,this是Proxy,如果你要拿到Vue實例(根組件),那麼就要用this._self
            //如何將current 做成動態的,current變化,對應的組件也應該跟着變化
			//vue實現雙向綁定 ,主要靠Object.defineProperty的get和set
            let current = this._self._root._router.history.current;
            let routeMap = this._self._root._router.routesMap;
			return h(routeMap[current]);
        }
    })

}
export default VueRouter;
發佈了92 篇原創文章 · 獲贊 23 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章