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;