Vue 技術棧 帶你探究 vue-router 源碼 手寫vue-router

寫在開頭

學習完了ES 6基礎,推薦閱讀:ECMAScript 6 全套學習目錄 整理 完結

現在開始逐步深入Vue 技術棧,想了想,技術棧專欄的主要內容包括:

1、Vue源碼分析
2、手把手教 保姆級 擼代碼
3、無懼面試,學以致用,繼承創新
4、談談前端發展與學習心得
5、手寫源碼技術棧,附上詳細註釋
6、從源碼中學習設計模式,一舉兩得
7、編程思想的提升及代碼質量的提高
8、通過分析源碼學習架構,看看優秀的框架
9、項目實戰開發
10、面試準備,完善個人簡歷


暫時想到的就這麼多,把這列舉的10點做好了,我覺得也OK了,歡迎一起學習,覺得不錯的話,可以關注博主,專欄會不斷更新,可以關注一下,傳送門~

學習目錄

爲了方便自己查閱與最後整合,還是打算整個目錄,關於Vue技術棧優秀的文章:

Vue 技術棧 手寫響應式原理 到 探索設計模式

Vue 技術棧 教你玩"壞" v8引擎 吃透 js 內存回收機制

正文

Vue路由的工作流程


前端路由和後端路由的區別

自從前後端分離後,說路由不再僅是說後端路由了,我們前端也有了路由。路由簡單來說,就是分發請求,將對應的請求分發到應該到的位置。

後端路由-mvc的時代:

  • 輸入url -》 請求發送到服務器 -》 服務器請求解析的路徑 -》 拿取對應頁面 -》 返回出去

前端路由-spa應用:

  • 輸入url -》js解析地址 -》 找到對應地址的頁面 -》 執行頁面生成的js -》 生成頁面

前端路由無需發送服務器,通過js進行解析,在瀏覽器上進行導向

vue-router 工作流程

vue插件

請讀者閱讀以下代碼,這就是vue-router的默認配置,最終返回給vue的是一個new VueRouter,也就是說是一個對象,而這個對象裏面就有我們之前圖示流程的current變量。

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  }
]

const router = new VueRouter({
  routes
})

export default router

vue 與 vue-router工作過程

再次回到我們的vue路由的工作流程,最下面三個部分是由vue-router來實現的,而上面是vue來工作的,vue一直監視着current變量,而vue-router能改變current,一旦改變,就會觸發監聽事件,根據current來獲取新的組件,然後vue去渲染新的組件,客戶就能看到新的界面了。總的來說就是:(兩個監聽,一個渲染)

上述文字類的表述或許不能讓你恍然大悟,接下來我們就化繁爲簡,將整個路由過程進行實現:

PS:但是在研究深入一點的知識前,爲了照顧小白,還是從基礎開始講起,已經熟悉的讀者可以選擇性閱讀。

hash 與 history


vue-router是怎麼觸發監聽事件的呢?

其實就是用到了hash,這裏對於前端來說就着重介紹hash了,history記得會有一定兼容性問題。

hash

1、#號後的就是hash的內容
2、可以通過location.hash拿到
3、可以通過onhashchange監聽hash的改變

history

1、history即正常的路徑
2、可以通過location.pathname拿到
3、可以通過onpopstate監聽history的改變

對於hash,我們可以在控制檯通過location.hash獲取值(如果沒有#就獲得空字符串),如下所示:

監聽hash的改變

window.onhashchange=function(){
	console.log('hash值已改變!')
}


history模式與上述方法類似

vue插件基礎知識

vue-router、vuex等其實都是屬於vue的插件,這些插件都是我們平常很多次使用的,下文將會循序漸進教你vue插件是如何開發的,我們怎樣開發一個vue插件

我們不管是使用vue-router還是vuex都會調用Vue.use()這個方法,如下圖所示,但是你有思考過Vue.use()到底是幹什麼用的呢?有什麼作用呢?

進行實踐,在main.js中我們進行如下操作,定義一個方法a,然後調用Vue.use()方法

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

function a(){
  console.log(6);
}

Vue.use(a);
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

執行結果:

從上述結果來看的話,我們給Vue一個方法,它就會執行一遍

那麼,我們給a一個install屬性,看看會打印什麼:

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

function a(){
  console.log(6);
}
a.install=function(){
	console.log('install!');
}
Vue.use(a);
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

執行結果:

Vue.use( ) 作用

從上述兩個例子來看,Vue.use()作用就是把你給的方法執行一遍,但如果有install屬性的話,會執行install屬性。

疑惑:如果只是爲了執行這個方法或者擁有install屬性的某個方法,那乾脆自己調用一下好了,爲啥還要用Vue.use()執行呢?

解決:其實,在install屬性的可以有一個參數傳進來,我們將上述代碼進行更改:

a.install=function(vue){
	console.log(vue);
}

打印結果:

ƒ Vue (options) {
  if ( true &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}

從打印結果來看的話,其實就是一個Vue的類,與下述代碼類似的一個類:

import Vue from 'vue'
Vue.mixin( )初識

對於Vue.use( )確實只是執行了一遍給的方法,但完成功能方面、起核心作用的還是vue.mixin()方法,

請看如下代碼,在main.js文件內,我們在vue.mixin()中混入data,裏面寫一個c

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

function a(){
  console.log(6);
}
a.install=function(vue){
	//console.log(vue);
	//全局混入vue實例
	vue.mixin({
		data(){
			return {
				c:'歡迎訪問超逸の博客'
			}
		}
	});
}
Vue.use(a);
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

然後在HelloWorld.vue組件內,顯示上文的c

打開界面,查看如下:

由上文可知,在HelloWorld組件裏是沒有c這個變量的,但是可以進行渲染顯示到我們的頁面,那麼mixin是可以混入全局變量,任何組件可以拿到mixin混入的實例

除了混入data外,我們還可以混入方法,舉個栗子:

a.install=function(vue){
	//console.log(vue);
	//全局混入vue實例
	vue.mixin({
		data(){
			return {
				c:'歡迎訪問超逸の博客'
			}
		},
		methods::{
			globalMethods:function(){
				
			}
		}
	});
}

那麼,其它組件都可以調用上述的方法,那麼這樣做有什麼好處呢?

一提及到全局可以使用,應該可以想到可複用性這個特點,比如我們開發常見的有些組件需要消息彈窗,可能大部分人會在每個組件進行import註冊等等,但是有了mixin()後,我們可以定義一個全局的方法,首先在App.vue寫好我們的消息彈窗的方法,用全局的方法去操作App.vue寫好的方法,那麼就有很高的複用性。

但是datamethods並不是我們mixin方法的關鍵,最牛的還是可以進行全局生命週期注入 比如createdbeforecreatedmounted等等

Vue插件開發一系列api(開始探索源碼)

console.log(Vue.util);

執行結果:

Vue.util.defineReactive

很重要的一個就是:Vue.util.defineReactive,它就是Vue監聽current變量重要執行者

不妨從源碼來學習:

/**
   * Define a reactive property on an Object.
   */
  //Vue的data監聽,也是通過這個方法
  function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
	//依賴收集者
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && observe(val);
	//雙向綁定
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
		  //進行依賴收集
          dep.depend();
		  /*採用依賴收集的原因:*/
          //1.data裏面的數據並不是所有地方都要用到
          //2.如果我們直接更新整個視圖,會造成資源浪費
		  //3.將依賴於某個變量的組件收集起來
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        //觸發依賴的組件產生更新
		dep.notify();
      }
    });
  }

雙向綁定

上述關於響應式 雙向綁定,強烈推薦之前寫過的一篇文章:

推薦閱讀:Vue 技術棧 手寫響應式原理 到 探索設計模式

手寫實現defineReactive

我們可以通過defineReactive來實現Vue監聽current的監視者,監聽某個第三方的變量

手寫:

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

//Vue插件開發一系列api
//console.log(Vue.util.defineReactive);
//test是屬於window的對象
var test={
	testa:'計時開始'
}
//設置定時器
setTimeout(function(){
	test.testa='計時結束'
},2000)
function a(){
  console.log(6);
}
a.install=function(vue){
	//console.log(vue);
	//監聽testa
	Vue.util.defineReactive(test,'testa');
	//全局混入vue實例
	vue.mixin({
		data(){
			return {
				c:'歡迎訪問超逸の博客'
			}
		},
		methods:{
		},
		beforeCreate:function(){
			this.test=test;
		},
		//全局生命週期注入
		created:function(){
			//console.log(this)
		}
		
	});
}
Vue.use(a);
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')

然後我們在HelloWorld組件進行渲染,查看頁面

執行結果:

疑問:爲什麼要寫在beforeCreate裏面?

解決:因爲create階段組件已經生成了,this實例已經創建了,而beforeCreate纔剛開始。這樣HelloWorld組件可以this調用來獲取testa的值

Vue.util.extend 與 Vue.extend 的區別

關於這個問題,我百度了一下,貌似很少有人去探究這個問題,既然查不到,那麼我們就從源碼來學習,這就是一個比較好的方法。源碼能夠給你答案

console.log(Vue.util.extend);
console.log(Vue.extend);
/**
   * Mix properties into target object.
   */
   
  //Vue.util.extend
  //其實就是拷貝一份,以後可以直接調用即可
  function extend (to, _from) {
    for (var key in _from) {
      to[key] = _from[key];
    }
    return to
  }
  
  /**
     * Class inheritance
     */
     
	//Vue.extend
    Vue.extend = function (extendOptions) {
      extendOptions = extendOptions || {};
      var Super = this;
      var SuperId = Super.cid;
      var cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {});
      if (cachedCtors[SuperId]) {
        return cachedCtors[SuperId]
      }
  
      var name = extendOptions.name || Super.options.name;
      if (name) {
        validateComponentName(name);
      }
  
      var Sub = function VueComponent (options) {
        this._init(options);
      };
      Sub.prototype = Object.create(Super.prototype);
      Sub.prototype.constructor = Sub;
      Sub.cid = cid++;
      Sub.options = mergeOptions(
        Super.options,
        extendOptions
      );
      Sub['super'] = Super;
  
      // For props and computed properties, we define the proxy getters on
      // the Vue instances at extension time, on the extended prototype. This
      // avoids Object.defineProperty calls for each instance created.
      if (Sub.options.props) {
        initProps$1(Sub);
      }
      if (Sub.options.computed) {
        initComputed$1(Sub);
      }
  
      // allow further extension/mixin/plugin usage
      Sub.extend = Super.extend;
      Sub.mixin = Super.mixin;
      Sub.use = Super.use;
  
      // create asset registers, so extended classes
      // can have their private assets too.
      ASSET_TYPES.forEach(function (type) {
        Sub[type] = Super[type];
      });
      // enable recursive self-lookup
      if (name) {
        Sub.options.components[name] = Sub;
      }
  
      // keep a reference to the super options at extension time.
      // later at instantiation we can check if Super's options have
      // been updated.
      Sub.superOptions = Super.options;
      Sub.extendOptions = extendOptions;
      Sub.sealedOptions = extend({}, Sub.options);
  
      // cache constructor
      cachedCtors[SuperId] = Sub;
      return Sub
    };
  }
單元測試

關於Vue.extend我們以下面這個單元測試例子來講解:

由下圖可知,我們獲取到了HelloWorld的構造函數,然後再拿到組件。簡單來說,你可以在任何地方,拿到任何組件,這對於單元測試方面是比較方便的,你可以拿到任何組件裏的方法進行測試。

手寫Vue-router(核心)

開始前準備
  • 在src下創建一個新的文件夾myrouter,新建一個index.js的文件

  • 將之前寫過的代碼都註釋掉,返回最初的模樣

  • 將VueRouter引用改爲我們自己所寫的myrouter

根據上文的流程圖,手寫vue-router

//記錄路由
class historyRouter{
	constructor() {
	    this.current=null;
	}
}
class vueRouter{
	constructor(options) {
	    this.mode=options.mode||'hash';
		this.routes=options.routes||[];
		this.history=new historyRouter;
		//創建routesMap 將數組形式的轉換成key-value形式的路由
		this.routesMap=this.createMap(this.routes);
		//事件監聽
		this.init();
	}
	init(){
		if(this.mode=='hash'){
			location.hash? '':location.hash='/';
			window.addEventListener('load',()=>{
				this.history.current=location.hash.slice(1);
			});
			window.addEventListener('hashchange',()=>{
				this.history.current=location.hash.slice(1);
			})
			
		}else{
			location.pathname? '':location.pathname='/';
			window.addEventListener('load',()=>{
				this.history.current=location.pathname;
			});
			window.addEventListener('popstate',()=>{
				this.history.current=location.pathname;
			})
		}
	}
	createMap(routes){
		return routes.reduce((memo,current)=>{
			memo[current.path]=current.component;
			return memo;
		},{})
	}
}

//Vue監視current變量
vueRouter.install=function(Vue){
	Vue.mixin({
		beforeCreate(){
			if(this.$options&&this.$options.router){
				this._root=this;
				this._router=this.$options.router;
				Vue.util.defineReactive(this,'current',this._router.history);
			}else{
				//嵌套路由,如果沒有路由,去找父組件
				this._root=this.$parent._root;
			}
		}
	})
	//獲取新組件以及render
	Vue.component('router-view',{
		//渲染新組件
		render(h){
			let current=this._self._root._router.history.current;
			//console.log(current);
			let routesMap=this._self._root._router.routesMap;.
			//console.log(routesMap);
			return h(routesMap[current]);
		}
	})
}
//將類暴露出去
export default vueRouter;

總結

對於最後手寫的vue-router讀者只要弄懂它的思想即可,作爲前端開發,我們不能只侷限於寫業務代碼,造輪子等,我們要提高我們的編程思維,弄懂其中的思想與原理,瞭解底層才能不是一個簡單的搬磚工!

附本篇學習源碼

鏈接:https://pan.baidu.com/s/11xAkcdSyMxGTCPyQJafPGg
提取碼:0z9j

(鏈接失效請評論區留言)


結尾

本篇文章是自學而寫,當然還會有很多不足的地方,希望您來指正,感激不盡!

學如逆水行舟,不進則退
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章