從mini源碼分析vue,優點特性總結,vue-cli知識點,以及vue項目的二次封裝,mini項目源碼附送(1W字+)

前言

在前端三大框架並存的今天,vue已是前端必須掌握的一部分。而對於不少入門者,或者轉行前端的小夥伴們,我的以爲vue是一個很是適合入門的框架的之一。筆者我的以爲,不管從api的易學的角度出發,仍是從原理層面解析,vue仍是比react的簡單一些。記得某個大神的面試分享:若是面試官沒有vue跟react方向的要求,儘可能往vue的方向扯,我的以爲是個很是優秀的意見哈哈哈。css

身處跳槽漲薪的年代,相信不少同行們都已經背了不少面經。(雖然心裏有點鄙視背題庫的人,面試神同樣,工做zhu同樣)。 長久的發展,仍是得紮紮實實的打好基礎。若是面試官再也不追問面經,反過來請你介紹vue,你想好怎麼介紹你的vue項目嗎?html

本文的重點,如何介紹如何搭建vue項目,介紹你的vue項目。前端

文章適合人羣

半年~三年經驗的vue開發者。 如未接觸過vue,建議從官方文檔:vuejs.org/ 學習搭建先。vue

該文章重點爲普及知識點,以及部分知識點的解析。html5

從mini源碼瞭解什麼vue

Vue.js是一套構建用戶界面的漸進式框架。 他最大的優點,也是單頁面最大的優點,數據驅動與組件化。node

首先咱們mini的源碼瞭解vue如何完成數據驅動。如圖:react

從圖咱們就能夠簡單的分析出什麼叫MVVM。jquery

MVVM, 實際上爲M + V + VM。vue的框架就是一個內置的VM狀態,而M就是咱們的MODLE, V便是咱們的視圖。而經過咱們的M,就能實現對V的控制,就是咱們所說的數據驅動(模型控制視圖)。ViewModel 的內容會實時展示在 View 層,前端開發者不再必低效又麻煩,還耗性能(由於沒有diff算法)地經過操縱 DOM 去更新視圖。這就是一個從根源上,MVVM框架比傳統MVC框架的優點。webpack

咱們進一步手寫Mini版來了解vue,從源碼瞭解什麼是數據劫持。ios

首先構造一個vue實例。寫過vue初始化的都知道,初始化時須要傳入data,以及綁定元素標記el。咱們把它儲存起來。

class wzVue {
	constructor(options){
		this.$options = options;
		this.$data = options.data;
		this.$el = options.el;
	}
}
複製代碼

首先看一下Observer的實現,以vue2.0爲例,咱們都知道數據劫持是經過Object.defineProperty。它自帶監聽get,set方法,咱們能夠用他實現一個簡單的綁定。

obsever(this.$data);

function obsever(){
      Object.defineProperty( obj, key, {
		get(){
			
		},
		set( newValue ){
			value = newValue;
		}
	})
}
複製代碼

這裏很簡單,若是仍是不明白怎麼雙向綁定,舉個簡單的栗子:

<input type="text" v-modle="key" id="key"/>

// script
var data = {
    key: 5
    key2: 8
}
obsever(data);
data.key=6;//

function obsever(obj){
      Object.defineProperty( obj, key, {
		get(){
			
		},
		set( newValue ){
		    document.getElementById('key').val(newValue);//寫死'key'先,下文會講解
		}
	})
}

//寫死'key'先,下文會講解
document.getElementById('key').addEventListener( 'click', false, function(e){
    obj.key = e.target.value;
})
複製代碼

這樣實現了雙向綁定,若是對象obj.key賦值,就會觸發set方法,同步input的數據;若是頁面手動輸入值,則經過監聽觸發set,同步到對象obj的值。此時你可能有一個疑問,咱們在vue賦值的時候,是直接修改上下文data數據的,並非修改對對象的值, 也就是this.key=6。是的,vue源碼中,先對data對象的數據進行了一次本地的數據劫持。以下文的proxyData。這樣的:

this.key ----> data.key(觸發) --->實現數據劫持

observer( data ){//監聽data數據,雙向綁定
    if( !data || typeof(data) !== 'object'){
		return;
	}
	Object.keys( data ).forEach( key => {
		this.observerData(key, data, data[key]);//監聽data對象
		this.proxyData( key );
	})
}

observerData( key, obj, value ) {
	this.observer(key);
	const dep = new Dep();
	Object.defineProperty( obj, key, {
		get(){
		},
		set( newValue ){ //通知變化
		}
	})
}

proxyData(key){
	Object.defineProperty( this, key, {
		get(){
			return this.$data[key];
		},
		set( newValue ){	
			this.$data[key] = newValue;
		}			
	})
}
複製代碼

兩點須要強調的地方:

1)遍歷data的屬性,vue的源碼是用了Object.keys。它能按順序遍歷出不一樣的屬性,可是不一樣的瀏覽器中可能執行順序不同。

2)由於Object.defineProperty只能監聽一層結構,因此,對於多層級的Object結構來說,須要遍歷去一層一層往下監聽。

那若是連續賦值的,例如this.key = 1; this.key2 = 2; 上邊的雙向綁定代碼是寫死了「key"。

這時候是否發生了兩次賦值?那麼咱們怎麼知道,它觸發的對象是哪一個呢?這時候,vue的設計是設計了dep的概念,來存放每一個監聽對象的值。

class Dep{
	constructor(){
		this.deps = [];
	}
	
	addDep(dep){
		this.deps.push(dep);
	}
	
	notiyDep(){
		this.deps.forEach(dep => {
			dep.update();
		})
	}
}
複製代碼

這裏不難理解。addDep既是爲了有數據變化時,插入的「對象」,表示須要劫持。 notiyDep便是該對象,已經須要被更新,執行對應的update方法。

那麼插入的對象是什麼呢(數組的單體)?單體確定,須要包含一個「dom」對象,還有對應監聽的「data」對象,二者關係綁定,才能實現數據同步。這個「單體」,咱們稱呼它爲「watcher」。

class Watcher{

	constructor( vm, key, initVal, cb ){
		this.vm = vm;//保存vue對象實例
		this.key = key;//保存綁定的key
		this.cb = cb;//同步二者的回調函數
		this.initVal = initVal;//初始化值
		this.vm[this.key];//觸發對象的get方法
	}
	
	update(){
		this.cb.call( this.vm, this.vm[this.key], this.initVal );
	}

}
複製代碼

截至目前爲止,obsever仍是沒有跟Watcher關聯上。在講他們怎麼關聯上以前,咱們再看看vue的設計思惟,它是由Watcher添加訂閱者,再由Dep添加變化。那麼Watcher是怎麼來的?從圖中的關係,咱們能夠看出由頁面解析出來的。這就是咱們要講的 Compile。

Compile,首先有一個「初始化視圖」的動做。

class Compile{
	
	constructor( el, vm ){
		this.$el  =  document.querySelector(el);
		this.$vm = vm;
		if (this.$el) {
			this.$fragment = this.getNodeChirdren( this.$el );
			this.$el.appendChild(this.$fragment);
		}
	}
	
	getNodeChirdren( el ){
		const frag = document.createDocumentFragment();
		let child;
		while( (child = el.firstChild )){
			frag.appendChild( child );
		}
		return frag;
	}
}
複製代碼

這裏應該不難理解,拿到template對象的id,遍歷完以後,賦值顯示在咱們的el元素中。接下來咱們重點講Compile產生的Watcher。咱們在Compile的原型中添加this.compile( this.$fragment);方法。對剛纔拿到template的模版進行繼續,看他用到哪些屬性。

compile( el ){
	const childNodes = el.childNodes;
	Array.from(childNodes).forEach( node => {
		if( node.nodeType == 1 ) {//1爲元素節點
			const nodeAttrs = node.attributes;
			Array.from(nodeAttrs).forEach( attr => {
				const attrName = attr.name;//屬性名稱
				const attrVal = attr.value;//屬性值
				if( attrName == "v-modle"){
				   	this.zDir_model( node, attrVal );
				}
			})
		} else if( node.nodeType == 2 ){//2爲屬性節點
			console.log("nodeType=====22");
		} else if( node.nodeType == 3 ){//3爲文本節點
			this.compileText( node );
		}
		// 遞歸子節點
		if (node.childNodes && node.childNodes.length > 0) {
			this.compile(node);
		}
	})
}
複製代碼

若是你對childNodes,nodeType,nodeList仍是一臉懵逼,建議移步到: 關於DOM和BOM知識點彙總: juejin.im/post/5efef0…

從上邊的mini源碼能夠看出,compile遍歷el的全部子元素,若是是文本類型,咱們就進行文本解析compileText。若是是input須要雙向綁定,咱們就進行zDir_model解析。

compileText( node ){
	if( typeof( node.textContent ) !== 'string' ) {
		return "";
	}
	const reg = /({{(.*)}})/;
	const reg2 = /[^/{/}]+/;
	const key = String((node.textContent).match(reg)).match(reg2);//獲取監聽的key
	const initVal = node.textContent;//記錄原文本第一次的數據
    updateText( node, this.$vm[key], initVal );
}

updateText( node, value, initVal ){
	var reg = /{{(.*)}}/ig;
	var replaceStr = String( initVal.match(reg) );
	var result = initVal.replace(replaceStr, value );
	node.textContent = result;
	new Watcher( this.$vm, key, initVal, function( value, initVal ){
		updateText( node, value, initVal  );
	});
}
複製代碼

咱們再看看compileText的源碼,大概意思爲,獲取到文本例如「個人名字{{name}}」的key,即爲name。而後name進行初始化賦值updateText, updateText的初始化結束後,添加訂閱數據變化,綁定更新函數Watcher。

而Watcher,正是綁定dep跟compile的橋樑。咱們修改一下添加到dep跟Watcher的代碼:

observerData( key, obj, value ) {
	this.observer(key);
	const dep = new Dep();
	Object.defineProperty( obj, key, {
		get(){
		    Dep.target && dep.addDep(Dep.target);//添加的代碼+++++++++++++++++
			return value;
		},
		set( newValue ){ //通知變化
		    if (newValue === value) {
			  return;
			}
			value = newValue;
			//通知變化
			dep.notiyDep();//添加的代碼+++++++++++++++++
		}
	})
}


class Watcher{

	constructor( vm, key, initVal, cb ){
		this.vm = vm;
		this.key = key;
		this.cb = cb;
		this.initVal = initVal;
		Dep.target = this;//添加的代碼+++++++++++++++++
		this.vm[this.key];
		Dep.target = null;
	}
	
	update(){
		this.cb.call( this.vm, this.vm[this.key], this.initVal );
	}

}
複製代碼

這樣的話,咱們在新增一個Watcher的過程當中,將此時的整個Watcher的this對象賦值到Dep.target中。這時候咱們再調用一下this.vm[this.key]。vm便是vue實例對象,因此,Watcher的this.vm[this.key],便是vue實例中的,this.key。而咱們的key已經經過Object.defineProperty監聽,此時就會進入到Object.defineProperty的get方法中, Dep.target 此時不爲空,因此dep.addDep(Dep.target),便是watcher添加訂閱者到dep中。

這時候若是數據發生變化,即調用set方法,而後dep.notiyDep,notiyDep就會通知,由文本解析的例如{{key}}的watcher從新更新一遍值,即完成了雙向綁定。

若是是v-modle的話,即在解析時,每一個對象多加一個監聽,而後主動調用set方法。

node.addEventListener("input", e => {
	  vm[value] = e.target.value;
	});
複製代碼

這就是vue整個雙向綁定的大體流程,所謂的數據驅動。

而後他有一個很大的缺陷,這個缺陷是,他知道驅動對象,卻沒法對數組進行驅動 (實際上也行) 。這裏vue的做者用了另一種思惟去解決這個問題。他重寫了數組的原型,把數組的'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'的方法重寫了一遍。也就是當你數組使用了這7個方法時,vue重寫的方法,會幫你變化中放入dep中。

這 個 (實際上也行) 其實也頗有學問。上述說,vue2.0沒法對數組進行數據監聽,其實真實的的測試中,Object.defineProperty是能夠監聽到數組變化的。可是隻能在已有的長度中,不能對其加長的長度。那你這時候可能會有疑問,那咱們重寫array的push方法就夠了,爲何要重寫7個呢?好吧,我也曾經有這樣的疑惑。後續,曾在帖子上,看到過vue的筆者回復過,印象中是這麼說的:Object.defineProperty對數組的監聽,消耗性能大於效果。也就是說,原本Object.defineProperty,爲了提高效率而產生,如今用在數組上,反而下降了效率,那不如干脆拒絕使用他。

因而,又有了vue-cli3.0數據劫持的改造。

那麼vue3.0是怎麼實現數據劫持的呢?

3.0中雙向綁定已經再也不是使用Object.defineProperty。而是proxy。proxy的引入,更高的效率,一方面解決了數組方面的問題,咱們能夠簡單看一下mini源碼的改造:

proxyData( data ){//監聽data數據,雙向綁定
	if( !data || typeof(data) !== 'object'){
		return;
	}
	const _this = this;
	const handler  = {
		set( target, key, value ) {
			const rest = Reflect.set(target, key, value);
			_this.$binding[key].map( item =>  {
				item.update();
			})
			return rest;
		}
	}
	this.$data = new Proxy( data, handler );
}
複製代碼

vue的mini源碼解析到此爲止,如還有不明白的地方可留言。 可須要源碼可進入github查看:github.com/zhuangweizh…

vue的特性是什麼

雙向綁定

由上述mini源碼,咱們能夠知道vue的數據驅動。MVVM相比MVC模式, 沒有頻繁的操做dom值,在開發中無疑時更高效的靈活頁面的觸發。可以讓咱們專一與邏輯js的抒寫,而具體的頁面變化,交給VM區處理。

diff算法

咱們都知道,js執行的效率高於dom渲染的效率。若是咱們能提早經過js算出不一致的地方,再最後去「渲染」最終的差別。明顯的增長效益。

咱們列出diff算法的三步曲:

  • 1)經過虛擬dom渲染對象
  • 2)對比兩個虛擬的差別
  • 3)根據差別進行渲染

全局混入mixins

mixins 選項接收一個混入對象的數組。而vue正是利用他來擴展vue的實例。

咱們的全局方法等,均可以利用mixins快速的套入vue實例。

完善的生命週期

十一個生命週期,create, mount, update, activated, destroyed。分別先後。最後還有v2.5.0版本的errorCaptured。 完善的生命週期的更適合,程序順序的正確執行。

豐富的組件傳遞

props, emit, slot,provide/inject,attrs/listeners,EventBus emit/on,parent / children與 ref

vue的優點是什麼

也是你爲何選擇vue的緣由

易學上手

筆者曾是一名jq的前端小雜,入門這些玩意,我的以爲他們的難度級別(僅限於api):

jq < 原生小程序 < vue系列 < angurle系列 < react系列

vue是剛開始一邊看着api就能夠擼出來的項目。

活躍的社區

也許每一個框架都有自身的bug。有bug不可怕,怕的是沒有解決方案。而vue中,你卡到問題點,但本身沒有能力解決時,活躍的社區會給你答案。

完善的第三方插件

支持axios, webpack, sass,elemnt-ui,vuex, router 等第三方插件。

支持客戶端全家桶

vue有着腳手架,ssr的nuxt框架,app版本的weex, 小程序多端開發uniapp。

可謂學好vue,吃遍前端全家桶。


最後:也許你以爲,上述react都支持。好的吧,的確是,晚些彙總完react的文章,再寫一篇對比。

vue-cli包含了什麼

vue-cli腳手架,幫咱們作了什麼。vue-cli3.0開始,已經成爲可選擇性的插件。咱們分析一下各個插件的做用。

webpack

blog.guowenfh.com/2016/03/24/…

webpack,打包全部的「腳本」。腳手架已經幫咱們經過webpack作了不少默認的loader。

咱們項目中,不一樣的文件,通過編譯輸出最終的html,js,css,都是通過webpack。

例如,編譯 ES2015 或 TypeScript 模塊成 ES5 CommonJS 的模塊;

再例如:編譯 SASS 文件成 CSS,而後把生成的CSS插入到 Style 標籤內,而後再轉譯成 JavaScript 代碼段,處理在 HTML 或 CSS 文件中引用的圖片文件,根據配置路徑把它們移動到任意位置,根據 MD5 hash 命名。

所以,咱們能夠不一樣文件,找在webpack不一樣的編譯器,如vue有vue-loader,腳手架幫咱們引入了。如sass有sass-loader,基本npm或者yarn生態圈中,已經有前端你全部見過的loader。也許還有沒有?不要緊,咱們能夠本身寫一個。

來個簡單的需求:開發環境過濾掉全部的打印。

這要是在傳統的項目,沒有通過編譯器,這是有多大的工做量。當有了咱們的webpack或者gulp等,他僅僅只是幾句代碼的問題。咱們來看一下webpack的實現:

配置文件:

const fs = require('fs');

function wzJsLoader(source) {
    /*過濾輸出*/
    const mode = process.env.NODE_ENV === 'production'
    if( mode ){//正式環境
        source = source.replace(/console.log\(.*?\);/ig, value => "" );
    }
    return source;
};

module.exports = wzJsLoader;
複製代碼

這樣,咱們就輕鬆定了一個本身的loader。在wepback.config.js,加上咱們對應的loader,輕鬆解決問題

{
    test: /\.js$/, //js文件加載器
    exclude: /node_modules/,
    use: [
        {
          loader: 'babel-loader?cacheDirectory=ture',
          options: {
            presets: ['@babel/preset-env']
          }
        },
        {
            loader: require.resolve("./myWebPackConfig/wz-js-loader"),//添加的
            options: { name: __dirname  },
        },
    ]
  },
複製代碼

webpack是個頗有難度的東西,本文就不繼續簡介,簡單瞭解webpack的配置,以及如何寫好Loader跟plugins等。若是你還有精力深刻,webpack的執行機制,如何打包成文件,他的生命週期等,均可以深刻挑戰,若是研究透徹,相信你的實力不通常。

axios

axios,網絡請求工具。提到網絡請求工具,你確定瞭解從ajax,fetch、axios。下邊這次講一下他們的發展史以及優缺點(具體何時,會在下文的「vue項目的二次封裝」中講解)

ajax,相信早期進入前端領域的人,都大爲喜歡。他基於jquery,對原生XHR的封裝,還支持JSONP,很是方便。 他的有點包括,無須要經過刷新頁面更新數據,支持異步與服務器通訊,並且規範被普遍支持。

當年可謂如「諾基亞」通常存在。惋惜「諾基亞」後來跌下神壇,ajax在網絡請求中也遭受的一樣的待遇。

那麼淘汰ajax的根本緣由是什麼呢?

由於引入的單頁面框架,如vue的mvvn架構,或者是隻有m的react,他們都屬於js驅動Html。這涉及到控制dom刷新的過程。es5能夠利用callback, 或者generater的迭代器模式進行處理。可是還不理想。因此es6引入了promise的概念。

因此,以返回promise的單位的異步控制進程逐步發展。

一方面,ajax沒有改進,他依然我行我素的不支持promise。這對「新」前端的理念很不符,咱們沒法用ajax來完成異步操做(除非回調地獄,寫過大項目的都知道定位問題太難了)。

另外一方面,他還須要引入jquery來實現。咱們都知道新框架,都基本脫離了jq。

SO,fetch就這樣產生了。解決了ajax沒法返回promise的問題。開始讓人拋棄ajax。

fetch號稱是ajax的替代品,它的API是基於Promise設計的,舊版本的瀏覽器不支持Promise,須要使用polyfill es6-promise

然而,fetch貌似是爲解決返回Promise而產生的,並無注意其餘網絡請求工具該作的細節,他雖然支持promise, 但暴露了太多的問題:

1)fetch只對網絡請求報錯,對400,500都當作成功的請求,服務器返回 400,500 錯誤碼時並不會 reject,只有網絡錯誤這些致使請求不能完成時,fetch 纔會被 reject。

2)fetch默認不會帶cookie,須要添加配置項: fetch(url, {credentials: 'include'})

3)fetch不支持abort,不支持超時控制,使用setTimeout及Promise.reject的實現的超時控制並不能阻止請求過程繼續在後臺運行,形成了流量的浪費

4)fetch沒有辦法原生監測請求的進度,而XHR能夠

所以,axios正式入場。他從新基於xhr封裝,支持返回Promise, 也解決了fetch的弊端。

反問:知道jquery,fetch,axios的區別了嗎?

vue-router

在沒有「路由」的概念時,咱們一般講「頁面路徑」。若是你經歷過spring mvc經過action映射到html頁面的時代,那麼恭喜 ,你已經使用過路由。他屬於後臺路由。後臺的路由,能夠簡單的理解成一個路徑的映射。

那麼有後臺路由,就會有前端路由。沒錯,帶來質的改變,就是前端路由。那麼他帶來的優點是什麼。

前端路由,又分hash模式跟history模式。咱們用兩張圖來簡單的說明一下,前端路由的原理:

hash模式

hash(#)是URL 的錨點,表明的是網頁中的一個位置,單單改變#後的部分,瀏覽器只會滾動到相應位置,不會從新加載網頁,也就是說hash 出如今 URL 中,但不會被包含在 http 請求中,對後端徹底沒有影響,所以改變 hash 不會從新加載頁面;同時每一次改變#後的部分,都會在瀏覽器的訪問歷史中增長一個記錄,使用」後退」按鈕,就能夠回到上一個位置;因此說Hash模式經過錨點值的改變,根據不一樣的值,渲染指定DOM位置的不一樣數據。hash 模式的原理是 onhashchange 事件(監測hash值變化),能夠在 window 對象上監聽這個事件。

優點呢?是否是很明顯?若是沒有使用異步加載,咱們的已經能夠不須要通過後臺,直接僅是頁面的「錨點」切換。

history模式

history模式充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。這兩個方法應用於瀏覽器記錄棧,在當前已有的 back、forward、go 基礎之上,它們提供了對歷史記錄修改的功能。只是當它們執行修改時,雖然改變了當前的 URL ,但瀏覽器不會當即向後端發送請求。

history模式充分利用 history.pushState API 來完成 URL 跳轉而無須從新加載頁面。

**此外,**vue的路由,還支持嵌套(多級)路由,支持路由動態配置,命名視圖(同一頁面多個路由),路由守衛, 過渡動態效果等,可謂功能十分之強大,考慮比較齊全,在此每一個列舉一個簡單的栗子:

路由動態配置:

const router = new VueRouter({
  routes: [
   動態路徑參數 以冒號開頭
    { path: '/detail/:id', component: Detail }
  ]
})
複製代碼

嵌套(多級)路由: const router = new VueRouter({

routes: [
        { path: '/detail/', component: User,
              children: [
                {
                  path: 'product',
                  component: Product  //二級嵌套路由
                },
              ]
        }
   ]
})
複製代碼

命名視圖:

<router-view></router-view>
<router-view name="a"></router-view>
<router-view name="b"></router-view>

const router = new VueRouter({
  routes: [
    {
      path: '/',
      components: {
        default: componentsDefulat,
        a: componentsA,
        b: componentsB
      }
    }
  ]
})
複製代碼

路由守衛:

router.beforeEach((to, from, next) => {
  // ...
})
複製代碼

動態效果:

<transition>
  <router-view></router-view>
</transition>
複製代碼

sass/less

sass跟less,二者都是CSS預處理器的佼佼者。

爲何要使用CSS預處理器?

CSS有具體如下幾個缺點

1.語法不夠強大,好比沒法嵌套書寫,致使模塊化開發中須要書寫不少重複的選擇器;

2.沒有變量和合理的樣式複用機制,使得邏輯上相關的屬性值必須以字面量的形式重複輸出,致使難以維護。

Less和Sass在語法上有些共性,好比下面這些:

  • 一、混入(Mixins)——class中的class;

  • 二、參數混入——能夠傳遞參數的class,就像函數同樣;

  • 三、嵌套規則——Class中嵌套class,從而減小重複的代碼;

  • 四、運算——CSS中用上數學;

  • 五、顏色功能——能夠編輯顏色;

  • 六、名字空間(namespace)——分組樣式,從而能夠被調用;

  • 七、做用域——局部修改樣式;

  • 八、JavaScript 賦值——在CSS中使用JavaScript表達式賦值。

再說一下二者的區別:

  • 1.Less環境較Sass簡單,使用起來較Sass簡單

  • 2.從功能出發,Sass較Less略強大一些 (1) sass有變量和做用域。

    (2) sass有函數的概念;

    (3) sass能夠進行進程控制。例如: -條件:@if @else; -循環遍歷:@for @each @while

    (4) sass又數據結構類型: -list類型=數組; -map類型=object; 其他的也有string、number、function等類型

  • 3.Less與Sass處理機制不同

  • 前者是經過客戶端處理的,後者是經過服務端處理,相比較之下前者解析會比後者慢一點。並且sass會產生服務器壓力。

vuex

vuex官方概念:Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。

看到這裏你可能會有疑問,咱們傳統的框架上,localstore, session , cookies,不就以及解決問題了麼。

沒錯。他們是解決了本地存儲的問題。可是vue是單頁面架構,須要數據驅動。 session , cookies沒法觸發數據驅動。這時候不得引入一個能夠監聽的容易。小型項目可能直接用store,或者頁面與頁面直接能夠用props傳遞

咱們在使用Vue.js開發複雜的應用時,常常會遇到多個組件共享同一個狀態,亦或是多個組件會去更新同一個狀態,在應用代碼量較少的時候,咱們能夠組件間通訊去維護修改數據,或者是經過事件總線來進行數據的傳遞以及修改。可是當應用逐漸龐大之後,代碼就會變得難以維護,從父組件開始經過prop傳遞多層嵌套的數據因爲層級過深而顯得異常脆弱,而事件總線也會由於組件的增多、代碼量的增大而顯得交互錯綜複雜,難以捋清其中的傳遞關係。

那麼爲何咱們不能將數據層與組件層抽離開來呢?把數據層放到全局造成一個單一的Store,組件層變得更薄,專門用來進行數據的展現及操做。全部數據的變動都須要通過全局的Store來進行,造成一個單向數據流,使數據變化變得「可預測」。

簡單說一下他的工做流程:

圖文相信已經很是清晰vuex的工做流程。簡單的簡述一下api:

state 簡單的理解就是vuex數據的儲存對象。

getters getter 會暴露爲 state 對象,你能夠以屬性的形式訪問這些值:

actions Action 相似於 mutation,不一樣在於: Action 提交的是 mutation,而不是直接變動狀態。 Action 能夠包含任意異步操做。

mutations 每一個 mutation 都有一個字符串的 事件類型 (type) 和 一個 回調函數 (handler)。 mutations能夠直接改變state的狀態。 mutations 不能夠包含任意異步操做

module Vuex 太大時,容許咱們將 store 分割成模塊(module)。每一個模塊擁有本身的 state、mutation、action、getter。

vuex的使用,很簡單。可是靈活時候,可能還須要進一步的瞭解源碼。vuex的原理其實跟vue有點像。

如須要看vuex源碼,可經過:github.com/zhuangweizh…

element ui/vux

即UI庫的選擇

vue的火熱,離不開vue社區的火熱。就常規的項目,若是公司不是要求特別高,基本各類UI庫,已經不須要你寫樣式(前端最煩的就是寫樣式沒意見吧)。

這裏就不作UI庫如何搭建的文章,有興趣能夠關注,後續我會寫一篇專門搭建UI庫的。

這裏介紹一下vue火熱的UI庫吧。

其中,移動端筆者推薦vant,管理後臺推薦element。

vue項目的二次封裝

axios的封裝

上文講解過axios的由來以及優缺點,這裏談談axios在vue項目的使用。

1)請求攔截

好比咱們的請求接口,全局都須要作token驗證。咱們能夠在請求錢作好token雁陣。若是存在,則請求頭自動添加token。

axios.interceptors.request.use(    
    config => {        
        // 每次發送請求以前判斷vuex中是否存在token        
        // 若是存在,則統一在http請求的header都加上token,這樣後臺根據token判斷你的登陸狀況
        // 即便本地存在token,也有可能token是過時的,因此在響應攔截器中要對返回狀態進行判斷 
        const token = store.state.token;        
        token && (config.headers.token = token);        
        return config;    
    },    
    error => {        
        return Promise.error(error);    
})
複製代碼

2)返回攔截

當程序異常的時候呢,接口有時候在特定的場景,或者是服務器異常的狀況下,是否就讓用戶白白等待? 若是有超時,錯誤返回機制,及時告知用戶的,是否是用戶好一點?這就是返回的攔截。

axios.interceptors.response.use(    
    response => {   
        // 若是返回的狀態碼爲200,說明接口請求成功,能夠正常拿到數據     
        // 不然的話拋出錯誤
        if (response.status === 200) {            
            return Promise.resolve(response);        
        } else {            
            return Promise.reject(response);        
        }    
    },    
    // 服務器狀態碼不是2開頭的的狀況
    // 這裏能夠跟大家的後臺開發人員協商好統一的錯誤狀態碼    
    // 而後根據返回的狀態碼進行一些操做,例如登陸過時提示,錯誤提示等等
    // 下面列舉幾個常見的操做,其餘需求可自行擴展
    error => {    
            alert("數據異常,請稍後再試或聯繫管理員");
            return Promise.reject(error.response);
        }
    }    
});
複製代碼

3)以get爲栗子

export function get(url, params){    
    return new Promise((resolve, reject) =>{        
        axios.get(url, {            
            params: params        
        }).then(res => {
            resolve(res.data);
        }).catch(err =>{
            reject(err.data)        
    })    
});
複製代碼

此外,對axios的使用還有想法的,建議查看一下axios全攻略: ykloveyxk.github.io/2017/02/25/…

編譯器改進

上文曾提到,vue-cli自帶webpack。那麼咱們如何經過他,來改進咱們的項目呢。

從環境區分,自帶的引入,已經幫咱們區分了環境,而後幫咱們導入不一樣的loader跟Pulger等,基本已是一個很是完善的編譯器。

咱們見到看一下dev的源碼(添加了註釋),dev環境,實際上會運行dev-server.js文件該文件以express做爲後端框架

// nodejs環境配置
var config = require('../config')
if (!process.env.NODE_ENV) {
  process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
var opn = require('opn') //強制打開瀏覽器
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware') //使用代理的中間件
var webpackConfig = require('./webpack.dev.conf') //webpack的配置

var port = process.env.PORT || config.dev.port //端口號
var autoOpenBrowser = !!config.dev.autoOpenBrowser //是否自動打開瀏覽器
var proxyTable = config.dev.proxyTable //http的代理url

var app = express() //啓動express
var compiler = webpack(webpackConfig) //webpack編譯

//webpack-dev-middleware的做用
//1.將編譯後的生成的靜態文件放在內存中,因此在npm run dev後磁盤上不會生成文件
//2.當文件改變時,會自動編譯。
//3.當在編譯過程當中請求某個資源時,webpack-dev-server不會讓這個請求失敗,而是會一直阻塞它,直到webpack編譯完畢
var devMiddleware = require('webpack-dev-middleware')(compiler, {
  publicPath: webpackConfig.output.publicPath,
  quiet: true
})

//webpack-hot-middleware的做用就是實現瀏覽器的無刷新更新
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
  log: () => {}
})
//聲明hotMiddleware無刷新更新的時機:html-webpack-plugin 的template更改以後
compiler.plugin('compilation', function (compilation) {
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({ action: 'reload' })
    cb()
  })
})

//將代理請求的配置應用到express服務上
Object.keys(proxyTable).forEach(function (context) {
  var options = proxyTable[context]
  if (typeof options === 'string') {
    options = { target: options }
  }
  app.use(proxyMiddleware(options.filter || context, options))
})

//使用connect-history-api-fallback匹配資源
//若是不匹配就能夠重定向到指定地址
app.use(require('connect-history-api-fallback')())

// 應用devMiddleware中間件
app.use(devMiddleware)
// 應用hotMiddleware中間件
app.use(hotMiddleware)

// 配置express靜態資源目錄
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))

var uri = 'http://localhost:' + port

//編譯成功後打印uri
devMiddleware.waitUntilValid(function () {
  console.log('> Listening at ' + uri + '\n')
})
//啓動express服務
module.exports = app.listen(port, function (err) {
  if (err) {
    console.log(err)
    return
  }
  // 知足條件則自動打開瀏覽器
  if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
    opn(uri)
  }
})
複製代碼

可見,webpack的編譯,以及相對完善。咱們也能夠去優化一下對應的插件,好比:

plugins: [
  new webpack.DefinePlugin({ // 編譯時配置的全局變量
    'process.env': config.dev.env //當前環境爲開發環境
  }),
  new webpack.HotModuleReplacementPlugin(), //熱更新插件
  new webpack.NoEmitOnErrorPlugin(), //不觸發錯誤,即編譯後運行的包正常運行
  new HtmlWebpackPlugin({  //自動生成html文件,好比編譯後文件的引入
    filename: 'index.html', //生成的文件名
    template: 'index.html', //模板
    inject: true
  }),
  new FriendlyErrorsPlugin() //友好的錯誤提示
]
複製代碼

最後講一下webpack的相關優化:

構建速度的優化:
  • 1.HappyPack 基於webpack的編譯模式本是單線程,時間佔時最多的Loader對文件的轉換。開啓HappyPack,能夠講任務分解成多個進程去並行處理。

    簡單配置:

    new HappyPack({// 用惟一的標識符 id 來表明當前的 HappyPack 是用來處理一類特定的文件 id: 'babel',// 如何處理 .js 文件,用法和 Loader 配置中同樣 loaders: ['babel-loader?cacheDirectory'],// ... 其它配置項 }),

    詳細可參考:www.fly63.com/nav/1472

  • 2.DllPlugin 可將一些Node_moudle一些編譯好的庫,經常使用並且不變的庫,抽出來。這樣就無需從新編譯。

  • 3.Loader 記錄配置搜索範圍,include,exclude 如:

    { test: /.js$/, //js文件加載器 exclude: /node_modules/, use: [ { loader: 'babel-loader?cacheDirectory=ture', options: { presets: ['@babel/preset-env'] }, include: Path2D.resolve(__dirname, 'src') } ] }

優化打包大小:
  • 1.tree shaking寫法。(webpack 4 已自動引入) 即「搖樹」。即只引入所須要引入部分,其他的代碼講會過濾。

  • 2.壓縮代碼 當前最程愫的壓縮工具是UglifyJS。HtmlWebpackPlugin也可配置minify等。

  • 3.文件分離 多個文件加載,速度更快。 例如:mini-css-extract-plugin,將css獨立出來。這樣還有利於,部分「不變」的文件作緩存。

  • 4.Scope Hoisting 開啓後,分細分出模塊直接的依賴關係,會自動幫咱們合併函數。簡單的配置:

    module,exports={ optimization:{   concatenateModules:true } }

組件化

任何框架,團隊都須要本身的組件化。(固然,有些團隊,怕人員的流動性,所有不組件化,最簡單的寫法,筆者也遇過這種公司)。

通常來講,組件大體能夠分爲三類:

  • 1)與業務無關的獨立組件。
  • 2)頁面級別的組件。
  • 3)業務上可複用的基礎組件。

關於1),能夠理解成如今的UI庫(如element/vant),這裏暫時不作獨立組件分析。(晚些可能會寫一篇如何寫獨立組件的文章,上傳到npm。)

關於2),貌似當某一個模塊,頁面須要屢次重複使用時候,就能夠寫成獨立組件,這個貌似沒什麼好分析。

這裏重點分析一下:** 3)業務上可複用的基礎組件 ** 。

筆者寫過的vue項目,都基本會封裝20~30個業務通用組件。例如截圖的my-form,my-table。以下:

這裏我覺得myTable

emelent的table插件,的確已經很強大了。可是筆者雖然用上了emelent ui,可是業務代碼卻沒有任何emelent的東西。

若是有一天,公司再也不喜歡element ui的table,那so easy,我把個人mytable修改一下,全部頁面即將同步。這就是組件化的魅力。

下邊我以my-table爲栗子,記錄一下我組件化的要點: 1.合併封裝分頁,是表格再也不關心分頁問題。 2.統一全局表格樣式(後期可隨時修改) 3.業務脫離,使業務上無需再關心element的api如何定義,且可隨時替換掉element。 4.自定義類型,本文提供select跟text控制,配置對象便可實現。 5.統一自定義缺省處理。 6.統一搜索按鈕,搜索框。配置對象便可實現。

這些優點,以及對全局的拓展性,是否是比傳統直接用的,有很大的優點?

固然,很差的地方,插件應該相對完善,考慮周全,須要一個全局統籌的人。對人員的流動的公司,的確很不友好。

下邊是源碼提供,可參考:

<template>
  <div>
    <h3 class="t_title">{{tName}}</h3>
    <div class="t_content">
      <el-form :inline="true" class="serach_form" >
        <el-form-item  v-for="(item, index) in tSerachList" :label="item.name" :key="index" v-if="tSerachList.length > 0 ">
          <div v-if="item.type == 'text'" >
            <el-input  :placeholder="item.name" v-model="tSerachList[index].value" ></el-input>
          </div>
          <div v-else-if="item.type == 'select'" >
            <el-select v-model="tSerachList[index].value"  :placeholder="item.name">
              <el-option  v-for="(cItem, cIndex) in item.list" :key="cIndex" :label="cItem.name" :value="cItem.value" ></el-option>
            </el-select>
          </div>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="onSubmit" >查詢</el-button>
        </el-form-item>
      </el-form>
      <!--按鈕操做模塊-->
      <el-row class="t_button_tab" v-for="(item,index) in tBtnOpeList" :key="index">
        <el-button  :type="item.type" @click="btnOpeHandle(item.opeList)" :render="item.render">{{item.label}}</el-button>
      </el-row>
    </div>

    <div class="t_table">
      <el-table
        :data="tableData"
        style="width: 100%">
        <el-table-column v-for="(item, index) in tableList" :key="index" v-bind="item">
          <template slot-scope="scope" >
            <my-table-render v-if="item.render" :row="scope.row" :render="item.render" ></my-table-render>
            <span  v-else>{{scope.row[item.key]}}</span>
          </template>
        </el-table-column>
      </el-table>
    </div>

    <div class="t_pagination">
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page.sync="currentPage"
        :page-size="tPageSize"
        layout="prev, pager, next, jumper"
        :total="tTotal">
      </el-pagination>
    </div>
  </div>

</template>

<script>

  import MyTableRender from './my-table-render.vue'

  export default {
    props: {
      tTablecolumn: { //展現的列名
        type: Array,
        required: true
      },
      tName: { //頁面的名稱
        type: String,
          required: true
      },
      tUrl: { //請求的URL
        type: String,
        required: true
      },
      tParam: { //請求的額外參數
        type: Object,
        required: true
      },
      tSerachList: { //接口的額外數據
        type: Array,
        required: true
      },
      tBtnOpeList: {
        type: Array,
        required: false
      }
    },
    data () {
      return {
        arrea: "",
        currentPage: 1,
        tableData: [],
        tableList: [],
        tTotal: 0,
        tPageSize: 10,
        serachObj: {} //搜索的文本數據
      }
    },
    created () {
      this.getTableList()
      this.reloadTableList()
    },
    methods: {
      async getTableList () {
        var Obj = { pageNum: this.currentPage, pageSize: this.tPageSize }
        var that = this;
        var url = this.tUrl;
        var param = Object.assign(this.tParam, Obj, this.serachObj)
        const res = await this.utils.uGet({ url:url, query:param })
        var list = res.data.dataList
        that.tableData = list
        that.tTotal = res.data.total
      },
      // 提交
      reloadTableList () {
        var tableList = this.tTablecolumn
        for (var i = 0; i < tableList.length; i++) {
          tableList[i].prop = tableList[i].key
          tableList[i].label = tableList[i].name
        }
        this.tableList = tableList
      },
      onSubmit ( res ) {
        var that = this;
        const status = this.$store.getters.getUserStatus;
        if( status == 4 ){
          this.utils.uErrorAlert("臨時用戶無權限哦");
        } else {
          this.utils.uLoading(800);
          var tSerachList = this.tSerachList;
          var obj = {}
          for (var i = 0; i < tSerachList.length; i++) {
            obj[ tSerachList[i].key ] = tSerachList[i].value;
          }
          this.serachObj = obj;
          this.currentPage = 1;
          this.getTableList();
        }
      },
      handleSizeChange () {
      },
      handleCurrentChange (obj) {
        this.currentPage = obj
        this.getTableList()
      },
      btnOpeHandle(params){
        const status = this.$store.getters.getUserStatus;
        if( status == 4 ){
          this.utils.uErrorAlert("臨時用戶無權限哦");
        } else {
          this.$emit('handleBtn', params);
        }
      }
    },
    components: {
      MyTableRender
    }
  }
</script>


<style  lang="scss">

  @import '@/assets/scss/element-variables.scss';
  .serach_form{
    background: $theme-light;
    text-align: left;
    padding-top: 18px;
    padding-left: 20px;
  }
  .t_title{
    /*float: left;*/
    /*padding: 20px;*/
    /*font-size: 23px;*/
    color:$theme;
    text-align: left;
    border-left: 3px solid $theme;
    padding-left: 5px;
  }

  .t_content{
    clear: both;
  }

  .t_table{
    clear: both;
    padding: 20px;
  }

  .t_pagination{
    margin-top: 20px;
    float: right;
    margin-right: 20px;
  }
  .t_button_tab{
    text-align: left;
    margin-top: 18px;
  }

</style>
複製代碼

mini項目源碼

最後送上我的手寫的mini版本vue源碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>wz手寫vue源碼</title>
</head>
<body>
  <div id="app" class="body" >
	<div class="b_header" >
		<img class="b_img" src="https://user-gold-cdn.xitu.io/2020/7/10/173344e271bf85af?w=400&h=400&f=png&s=3451" /><span>wz手寫vue源碼</span>
	</div>
	<div class="b_content" >
		<div class="n_name" >姓名:{{name}}</div>
		<div class="box" >
			<span>年齡:{{age}}</span>
		</div>
		<div>{{content}}</div>
		<div>
			<input type="text" wz-model="content" placeholder="請輸入自我介紹"  />
		</div>
		<div wz-html="htmlSpan" ></div>
		<button @click="changeName" >點擊提示</button>
	</div>
  </div>
  <style>
  .body{
	 text-align: left;
	 width: 300px;
	 margin: 0 auto;
	 margin-top: 100px;
  }
  .body .b_header{
	display:  flex;
	justify-item: center;
	justify-content: center;
	align-items: center;
	align-content: center;
	margin-bottom: 20px;
  }
  .body .b_header span{
	font-size: 21px;
  }
  .body .b_img{
	display: inline-flex;
	width: 20px;
	height: 20px;
	align-item: center;
  }
  .body .b_content{
	
  }
  .body div{
	margin-top: 10px;
	min-height: 20px;
  }
  button{
	margin-top: 20px;
  }
  </style>
  <script src="./wzVue.js"></script>
  <script>
    const w = new wzVue({
      el: '#app',
      data: {
        "name": "加載中...",
        "age": '加載中...',
		"content": "我是一枚優秀的程序員",
		"htmlSpan": '<a href="http://wwww.zhuangweizhan.com">點擊歡迎進入我的主頁 </a>'
      },
      created() {
        setTimeout(() => {
          this.age = "25歲";
		  this.name = "weizhan";
        }, 800);
      },
	  methods: {
		changeName() {
			alert("歡迎進入我的主頁: http://www.zhuangweizhan.com");
		}
	  }
    })
  </script>
</body>
</html>


// js文件
/*
	本代碼來自weizhan
*/
class wzVue {
	constructor(options){
		this.$options = options;
		console.log("this.$options===" + JSON.stringify(this.$options) );
		this.$data = options.data;
		this.$el = options.el;
		this.observer( this.$data );//添加observer監聽
		new wzCompile( options.el, this);//添加文檔解析
		if ( options.created ) {
			options.created.call(this);
		}
	}
	
	observer( data ){//監聽data數據,雙向綁定
		if( !data || typeof(data) !== 'object'){
			return;
		}
		Object.keys(data).forEach(key => {//若是是對象進行解析
			this.observerSet(key, data, data[key]);//監聽data對象
			this.proxyData(key);//本地代理服務
		});
	}
	
	observerSet( key, obj, value ){
		this.observer(key);
		const dep = new Dep();
		Object.defineProperty( obj, key, {
			get(){
				Dep.target && dep.addDep(Dep.target);
				return value;
			},
			set( newValue ){
				if (newValue === value) {
				  return;
				}
				value = newValue;
				//通知變化
				dep.notiyDep();
			}
		})
	}
	
	proxyData(key){
		Object.defineProperty( this, key, {
			get(){
				return this.$data[key];
			},
			set( newVal ){
				this.$data[key] = newVal;
			}
		})	
	}
	
}

//存儲數據數組
class Dep{
	constructor(){
		this.deps = [];
	}
	
	addDep(dep){
		this.deps.push(dep);
	}
	
	notiyDep(){
		this.deps.forEach(dep => {
			dep.update();
		})
	}
}

//我的編譯器
class wzCompile{
	constructor(el, vm){
		this.$el = document.querySelector(el);
		
		this.$vm = vm;
		if (this.$el) {
			this.$fragment = this.getNodeChirdren( this.$el );
			this.compile( this.$fragment);
			this.$el.appendChild(this.$fragment);
		}
	}
	
	getNodeChirdren(el){
		const frag = document.createDocumentFragment();
		
		let child;
		while( (child = el.firstChild )){
			frag.appendChild( child );
		}
		return frag;
	}
	
	compile( el ){
		const childNodes = el.childNodes;
		Array.from(childNodes).forEach( node => {
			if( node.nodeType == 1 ) {//1爲元素節點
				const nodeAttrs = node.attributes;
				Array.from(nodeAttrs).forEach( attr => {
					const attrName = attr.name;//屬性名稱
					const attrVal = attr.value;//屬性值
					if( attrName.slice(0,3) === 'wz-' ){
						var tagName = attrName.substring(3);
						switch( tagName ){
							case "model":
								this.wzDir_model( node, attrVal );
							break;
							case "html":
								this.wzDir_html( node, attrVal );
							break;
						}
					}
					if( attrName.slice(0,1) === '@'  ){
						var tagName = attrName.substring(1);
						this.wzDir_click( node, attrVal );
					}
				})
			} else if( node.nodeType == 2 ){//2爲屬性節點
				console.log("nodeType=====22");
			} else if( node.nodeType == 3 ){//3爲文本節點
				this.compileText( node );
			}
			
			// 遞歸子節點
			if (node.childNodes && node.childNodes.length > 0) {
				this.compile(node);
			}
		})
	}
	
	wzDir_click(node, attrVal){
		var fn = this.$vm.$options.methods[attrVal];
		node.addEventListener( 'click', fn.bind(this.$vm));
	}
	
	wzDir_model( node, value ){
		const vm = this.$vm;
		this.updaterAll( 'model', node, node.value );
		node.addEventListener("input", e => {
		  vm[value] = e.target.value;
		});
	}
	
	wzDir_html( node, value ){
		this.updaterHtml( node, this.$vm[value] );
	}
	
	updaterHtml( node, value ){
		node.innerHTML = value;
	}
	
	compileText( node ){
		if( typeof( node.textContent ) !== 'string' ) {
			return "";
		}
		console.log("node.textContent===" + node.textContent  );
		const reg = /({{(.*)}})/;
		const reg2 = /[^/{/}]+/;
		const key = String((node.textContent).match(reg)).match(reg2);//獲取監聽的key
		this.updaterAll( 'text', node, key );
	}
	
	updaterAll( type, node, key ) {
		switch( type ){
			case 'text':
				if( key ){
					const updater = this.updateText;
					const initVal = node.textContent;//記錄原文本第一次的數據
					updater( node, this.$vm[key], initVal);
					new Watcher( this.$vm, key, initVal, function( value, initVal ){
						updater( node, value, initVal  );
					});
				}
				break;
			case 'model':
				const updater = this.updateModel;
				new Watcher( this.$vm, key, null, function( value, initVal ){
					updater( node, value );
				});
				break;
		}
	}

	updateModel( node, value ){
		node.value = value;
	}
	
	updateText( node, value, initVal ){
		var reg = /{{(.*)}}/ig;
		var replaceStr = String( initVal.match(reg) );
		var result = initVal.replace(replaceStr, value );
		node.textContent = result;
	}
	
}

class Watcher{
	
	constructor( vm, key, initVal, cb ){
		this.vm = vm;
		this.key = key;
		this.cb = cb;
		this.initVal = initVal;
		Dep.target = this;
		this.vm[this.key];
		Dep.target = null;
	}
	
	update(){
		this.cb.call( this.vm, this.vm[this.key], this.initVal );
	}

}
複製代碼

文章結尾

文章均爲原創手寫,寫一篇原創上萬字的文章,明白了筆者的不易。

若有錯誤但願指出。

後續,我會繼續react的總結。

相關文章
相關標籤/搜索