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

寫在開頭

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

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

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


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

學習目錄

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

正文

Vue 2的響應式原理


提到Vue2的響應式原理,或許你就會想到Object.defineProperty(),但Object.defineProperty()嚴格來說的話,並不是來做響應式的。

什麼是defineProperty( )

推薦閱讀:Vue 中 數據劫持 Object.defineProperty()

  • defineProperty其實是定義對象的屬性,或者你可以認爲是對象的屬性標籤
defineProperty其實並不是核心的爲一個對象做數據雙向綁定,而是去給對象做屬性標籤,只不過屬性裏的get和set實現了響應式
屬性名 默認值
value undefined
get undefined
set undefined
writalbe true
enumerable true
configurable true

下面我們來詳細瞭解一下:

var obj={
    a:1,
    b:2
}
//參數說明:1.對象 2.對象的某個屬性 3.對於屬性的配置
Object.defineProperty(obj,'a',{
	writable:false
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));

打開瀏覽器,按F12,將以上代碼粘貼過去,查看控制檯內容:


上述,打印的就是我們obj對象中a屬性的一系列標籤,權限方面可以看到默認的話爲true

那麼,我們剛剛設置了 writalbe爲false,即設置了a屬性不可寫,進行簡單測試一下:

發現我們無法對a屬性進行value的修改,因爲將writalbe設置了爲false

當然,我們可以設置其他權限標籤,例如:

var obj={
    a:1,
    b:2
}
//參數說明:1.對象 2.對象的某個屬性 3.對於屬性的配置
Object.defineProperty(obj,'a',{
	writable:false,
	enumerable:false,
	configurable:false
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));

因此,承接上文所述,defineProperty並不是來做響應式的,而是給對象中某個屬性設置權限操作,是否可寫,是否可以for in,是否可delete


get和set的使用

Vue中實現雙向綁定,其實就是與get和set有很大關係

舉個栗子,請看如下代碼:

var obj={
    a:1,
    b:2
}
//參數說明:1.對象 2.對象的某個屬性 3.對於屬性的配置
Object.defineProperty(obj,'a',{
	get:function(){
		console.log('a is be get!');
	},
	set:function(){
		console.log('a is be set!');
	}
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));

我們在控制檯,簡單測試一下:

問題來了,細心的夥伴,應該發現了上圖的問題,當我們get的時候,我們返回的是一個undefined,而且我們set一個值之後,也是獲取不到新值,依舊是undefined,如下:

原因呢,其實就是我們的get函數是有返回值的,如果你不return的話,就會默認返回undefined,不管你怎麼set都沒用,那麼如何解決這個問題呢,請看下面代碼:

var obj={
    a:1,
    b:2
}
//藉助外部變量存儲值
let _value=obj.a;
//參數說明:1.對象 2.對象的某個屬性 3.對於屬性的配置
Object.defineProperty(obj,'a',{
	get:function(){
		console.log('a is be get!');
		return _value;
	},
	set:function(newVal){
		console.log('a is be set!');
		_value=newVal;
		return _value;
	}
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));

可以看到,我們必須藉助一個外部變量,也就是中轉站一樣,才能達到我們的get和set效果,這也是vue2 中不太優雅的地方

然後,查看控制檯,解決了上述問題

Vue中從改變一個數據到發生改變的過程

手寫 Vue 2 中響應式原理

基於上述流程圖,我們可以手寫一個簡單版的Vue2.0實現雙向綁定的例子:

這裏我就只實現邏輯,不具體去弄視圖渲染了

文件名:2.js

//Vue響應式手寫實現
function vue(){
	this.$data={a:1};
	this.el=document.getElementById('app');
	this.virtualdom="";
	this.observer(this.$data)
	this.render();
}
//註冊get和set監聽
vue.prototype.observer=function(obj){
	var value; //藉助外部變量
	var self=this; //緩存this
	
	/*下面代碼 a可能是data裏的某個對象,不是屬性
	因此在vue2.0中需要for in循環找到屬性*/
	//Object.defineProperty(obj,'a') 
	
	for(var key in obj){
		value=obj[key];
		//判斷是否爲對象
		if(typeof value === 'object'){
			this.observer(value);
		}else{
			Object.defineProperty(this.$data,key,{
				get:function(){
					//進行依賴收集
					return value;
				},
				set:function(newVal){
					value=newVal;
					//視圖渲染
					self.render();
				}
			}) 
		}
	}
}
//更新渲染部分
vue.prototype.render=function(){
	this.virtualdom="i am "+this.$data.a;
	this.el.innerHTML=this.virtualdom;
}

文件名:index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>手寫Vue響應式原理</title>
	</head>
	<body>
		<div id='app'></div>
		<script type="text/javascript" src="./2.js"></script>
		<script type="text/javascript">
			var vm = new vue();
			//設置set定時器
			setTimeout(function(){
				console.log('2秒後將值改爲123');
				console.log(vm.$data);
				vm.$data.a=123;
			},2000)
		</script>
	</body>
</html>

查看頁面,就會有如下效果:

那麼,以後面試如果遇到手寫響應式原理,把上述js代碼寫上去就ok了

源碼分析:響應式原理中的依賴收集

手寫的代碼裏面對於依賴收集這一塊我們進行了省略,下面我們從源碼的角度去看依賴收集到底是什麼玩意:

/**
   * Define a reactive property on an Object.
   */
  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();
      }
    });
  }

對依賴收集的總結

在初次渲染時,會觸發一次get函數,爲了提高效率,節省資源,採用依賴收集,這裏以之前手寫的爲例,get部分,我們就會對this.$data裏的每一個屬性(即key值)進行收集,看在哪些組件裏進行了調用,以此提高效率。

而在set部分,就會更新我們收集到的依賴

	Object.defineProperty(this.$data,key,{
				get:function(){
					//進行依賴收集
					return value;
				},
				set:function(newVal){
					value=newVal;
					//視圖渲染
					self.render();
				}
			}) 

額外注意——關於數組的監聽(探索設計模式)

從前文我們可以瞭解到,defineProperty定義的ger和set是對象的屬性,那麼數組該怎麼辦呢?

對於數組呢,在Vue中,你是沒有辦法像C/C++、Java等語言那樣直接通過操作下標來觸發更新,只能通過push、pop等方法來觸發數據更新

var arr=[1,2,3];
arr.push(4);
arr.pop();
arr.shift();

這裏 特別重要!

關於數組這一塊裏面巧妙運用到了一個設計模式——裝飾者模式

//裝飾者模式

//先取出原型
var arraypro=Array.prototype;
//拷貝一份,原因:避免影響到了原來的原型鏈
var arrob=Object.create(arraypro);
//定義一個需要裝飾的方法的數組,這裏只例舉以下三個
var arr=['push','pop','shift'];
//設置重寫方法(裝飾者模式)
arr.forEach(function(methods,index){
	arrob[method]=function(){
		//先調用原來的方法
		var res=arraypro[method].apply(this,arguments);
		//觸發視圖更新
		dep.notify();
	}
})
//接下來將數組的prototype替換到data上的prototype(此處省略)
//這樣的話,例如我們push方法,既能push又能觸發視圖更新了

對於設計模式呢,其實並不是很難,常說難懂,很難學,可能你學設計模式,你看了書,看到的可能就是簡單事例,只是一個用法,沒有訓練思維,正確的做法是:

  • 提高我們的思維,提高代碼質量
  • 先學透,記住一些定義和一些具體使用,然後去看,去探索
  • 非常好的一種方式就是結合源碼,例如上文我們從Vue數組的監聽原理裏面剖析出來了裝飾者模式
  • 學以致用

Vue 3的響應式原理


對於2.0響應式原理,我們暫告一段落,接下來,我們討論Vue 3中的技巧,衆所周知,Vue 3將defineProperty替換成了proxy

什麼是proxy

用於定義基本操作的自定義行爲

和defineProperty類似,功能幾乎一樣,只不過用法上有所不同

和上文一樣,我們依舊寫一個響應式,不過下面的代碼是有問題的,讀者可以先思考一下。

var obj={
    a:1,
    b:2
}
//無需藉助外部變量

new Proxy(obj,{
	get(target,key,receiver){
		console.log(target,key,receiver);
		return target[key];
	},
	set(target,key,value,receiver){
		return Reflect.set(target,key,value);
		//return target[key]=value;
		/*上面註釋的代碼和上一行意思相同*/
	}
})

我們在控制檯跑一下上述代碼,發現它並沒有輸出console.log的內容,因此是有問題的

正確代碼如下:

var obj={
    a:1,
    b:2
}
//無需藉助外部變量
//對於vue 2,提高效率,無需for in 遍歷找屬性
//不會污染原對象,會返回一個新的代理對象,原對象依舊是原對象
//也是軟件工程裏的重要知識,儘量不要"污染"原對象,不用給原對象做任何操作
//只需對代理對象進行操作
var objChildren=new Proxy(obj,{
	get(target,key,receiver){
		console.log(target,key,receiver);
		return target[key];
	},
	set(target,key,value,receiver){
		return Reflect.set(target,key,value);
		//return target[key]=value;
		/*上面註釋的代碼和上一行意思相同*/
	}
})

總結:爲什麼Vue 3中使用proxy

  • defineProperty只能監聽某個屬性,不能對全對象進行監聽
  • 可以省去for in遍歷找對象中的屬性,提高效率,省去很多代碼
  • 可以監聽數組,不用再去單獨的對數組進行特異性操作
  • 不會污染原對象,會返回一個新的代理對象,原對象依舊是原對象
  • 只需對代理對象進行操作

手寫 Vue 3 中響應式原理

下面代碼,是在上文手寫 Vue 2 響應式原理基礎上修改的,通過對比,可以發現,我們省去了好多代碼,不需要進行for in循環比較複雜、耗時間的操作了

//Vue響應式手寫實現
function vue(){
	this.$data={a:1};
	this.el=document.getElementById('app');
	this.virtualdom="";
	this.observer(this.$data)
	this.render();
}
//註冊get和set監聽
vue.prototype.observer=function(obj){
	var self=this;
	this.$data=new Proxy(this.$data,{
		get(target,key){
			return target[key];
		},
		set(target,key,value){
			target[key]=value;
			self.render();
		}
	})
}
//更新渲染部分
vue.prototype.render=function(){
	this.virtualdom="i am "+this.$data.a;
	//this.el.innerHTML=this.virtualdom;
	this.el.innerHTML=this.virtualdom;
}

查看頁面,就會有如下效果:

proxy這麼好用,還能做什麼呢?(再遇設計模式)

我們學習知識並不只是爲了應付面試那種程度,對於面試應該作爲我們的最低要求,接下來,我們接着去深度研究proxy還能幹什麼呢?

在 Vue 3 基本上已經不兼容IE8了,這裏簡單提及一下

  • 類型驗證

這裏我們就自定義一個實例:創建一個成人的對象,擁有name和age兩個屬性

要求:name必須是中文,age必須是數字,並且大於18

如果用純原生js做驗證的話,可想有多難去驗證上述需求,或許你想到的是在構造函數裏面去實現,但也不會簡單,那麼我們看看proxy怎麼實現的:

//類型驗證
//外部定義一個驗證器對象
var validator={
	name:function(value){
		var reg=/^[\u4E00-\u9FA5]+$/;
		if(typeof value=='string'&&reg.test(value)){
			return true;
		}
		return false;
	},
	age:function(value){
		if(typeof value=='number'&&value>=18){
			return true;
		}
		return false;
	}
}

function person(name,age){
	this.name=name;
	this.age=age;
	return new Proxy(this,{
		get(target,key){
			return target[key];
		},
		set(target,key,value){
			if(validator[key](value)){
				return Reflect.set(target,key,value);
			}else{
				throw new Error(key+' is not right!');
			}
		}
	})
}

這裏 特別重要!

關於類型驗證這一塊裏面又巧妙運用到了一個設計模式——策略模式

關於設計模式這一塊,此專欄不會細講,但會在探索源碼時發現了好的實例,會提出來一下。

上述用到了一個正則表達式,關於這個可能面試會問到,這是之前ES 6 裏的內容,大家可以看看這篇簡單易懂的文章:

推薦閱讀:ES6 面試題:你能說出瀏覽器上到此支持多少箇中文字嗎?

  • 私有變量

關於私有變量這一塊,我們就拿 vue-router 源碼來進行分析:

//vue-router源碼分析

Object.defineProperty(this,'$router',{//Router的實例
	get(){
		return this._root._router;
	}
});
Object.defineProperty(this,'$route',{
	get(){
		return{
			//當前路由所在的狀態
			current:this._root._router.history.current
		}
	}
})

通過查看源碼,提出疑問:爲什麼要爲$router寫get方法呢,而且沒做什麼操作,只是一個return?

原因:這樣可以使得$router不可修改。避免程序員通過set修改了路由,導致路由失效的情況。這裏就體現了數據安全思想,前端程序員或許考慮的沒有Java程序員多,甚至沒有爲變量想過某個變量設置不可修改。由於工作的需要,我們也要努力提升自己的代碼質量!讓自己的職業生涯更加輝煌!

virtual dom 和 diff算法


關於diff算法和虛擬dom,也是面試常見的問題,平常容易忽視,這裏我也就深入研究了一下:

虛擬dom

所謂虛擬dom,如字面意思,它是虛擬的,只在概念裏面存在,並不真的存在,在vue中是ast語法樹,關於這個語法樹本文就不詳細介紹了,有興趣的讀者可以深入研究一下。

下面代碼,是一個簡單vue template模板,那麼解析成虛擬dom是怎樣的呢?

<template>
	<div id='dd'>
		<p>{{msg}}</p>
		<p>abc</p>
		<p>123</p>
	</div>
</template>

解析成虛擬dom:

diff <div>
	props:{
		id:dd
	},
	children:[
		diff <p>
		props:
		children:[
			
		],
		text:xxx,
	]

上述代碼就是概念上的介紹,如果懂一點算法知識的應該就明白了,就是不斷地嵌套,但爲了讓更多夥伴讀懂學會虛擬dom,下面來手寫一個對象的形式:

<template>
	<div id='dd'>
		<p><span></span></p>
		<p>abc</p>
		<p>123</p>
	</div>
</template>

var virtual=
{
	dom:'div',
	props:{
		id:dd
	},
	children:[
		{
			dom:'p',
			children:[
				dom:'span',
				children:[]
			]
		},
		{
			dom:'p',
			children:[
			]
		},
		{
			dom:'p',
			children:[
			]
		}
	]
}

上述代碼應該就很清晰了,簡單來說,就是將最上面的dom結構,解析成下面用js解析成的對象,每一個對象都有一個基礎的結構:

  • dom元素標籤
  • props記錄掛載了哪些屬性
  • children記錄有哪些子元素(子元素擁有和父元素相同的結構)

diff算法的比對機制

下面部分採用了僞代碼形式介紹diff算法的比對機制,已經給出了詳細的註釋說明:

//diff算法匹配機制
patchVnode(oldVnode,vnode){
	//先拿到真實的dom
	const el=vnode.el=oldVnode.el;
	//分別拿出舊節點和新節點的子元素
	let i,oldCh=oldVnode.children,ch=vnode.children;
	//如果新舊節點相同,直接return
	if(oldVnode==vnode) return;
	/*分四種情況討論*/
	//1.只有文字節點不同的情況
	if(oldVnode.text!==null&&vnode.text!==null&&oldVnode.text!==vnode.text){
		api.setTextContent(el,vnode.text);
	}else{
		updateEle();
		//2.如果新舊節點的子元素都存在,那麼發生的是子元素變動
		if(oldCh&&ch&&oldCh!==ch){
			updateChildren();
		//3.如果只有新節點有子元素,那麼發生的是新增子元素
		}else if(ch){
			createEl(vnode);
		//4.如果只有舊節點有子元素,那麼發生的是新節點刪除了子元素
		}else if(oldCh){
			api.removeChildren(el);
		}
	}
}

總結

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