原生javascript 100行js代碼實現一個mvvm框架

一,基礎知識

1,何爲MVVM(雙向數據綁定)

雙向數據綁定(MVVM):數據(M)發生變化時立即影響視圖(V),而視圖(V)發生改變也會立即影響數據(M)

2,實現數據綁定的方法

實現數據綁定的做法有大致如下幾種:

1,觀察者模式(backbone)

發佈者發佈事件,觀察者監聽事件。當某些方法被觸發時,就通知觀察者執行預定操作。
觀察者可以使用自己寫也可以使用es7最新添加的數據綁定方法Object.observe()。資料參考
https://www.w3ctech.com/topic/1097

2,髒值檢查(angular)

angular就是通過髒值檢測實現的。首先angular會解析dom中的命令,然後記錄所有變量的當前值,當發生觸發操作之後,通過apply或者digest進入髒檢查環節。上次記錄值與當前值是否一致。不一致就更新視圖,然後再髒值檢測一次直到數據不再發生變化。如果一致就不做任何操作

3,設置屬性訪問器(vue)

通過Object.defineProperty()來獲取每個屬性的setter,getter,在數據變動時通知訂閱者進行處理。

二,動手實現一個MVVM框架

1,目標與使用規則

我們想要實現的效果是這樣的:

這裏寫代碼片

使用類vue的定義方式,即通過自定義元素屬性來進行數據標記。el綁定html的作用域,data綁定數據,@+事件名綁定方法。
任何一個綁定了數據"data"(data=“say”)的輸入框的內容發生變動,都將即時更新所有綁定了"data"數據的網頁元素。點擊綁定了事件(@click=“go()”)的元素,會觸發與點擊事件綁定的方法(“go()”) 。

<!--html部分:-->
<div id="mvEl">
	<p id="label" data="say">這個p標籤綁定了數據"say",這裏綁定的是innerText</p>
	<input id="input" type="text" value='這個輸入框的值也綁定了數據"say"' data="say" />
	<div @click="go" data="say">這個div標籤綁定了點擊事件"go"。
		<input id="input2" type="text" value="這是一個嵌套在div標籤中的輸入框,它也綁定了數據say,這裏綁定的是value" data="say" />
	</div>
</div>

<!--js部分:-->
<script type="text/javascript">
//實例化mvvm
var vm = new mvvm({
	//綁定域
	el: "mvEl",
	//綁定數據
	data: {
		say: "這是數據1",
		say1: "這是數據2"
	},
	//動作方法
	action: {
		go: () => {
			vm.data.say+="廢話";
			console.info("這是個方法呀");
		}
	}
});
<script>
2,發佈訂閱

爲每個綁定的元素設置訪問器,當setter觸發時就通知所有的訂閱者,以此達到即時更新的效果。

			
			//發佈器   
			function observe(data, dep) {
				if (!data || typeof data !== 'object') {
					return;
				}
				// 獲取data中的所有屬性(say,say1)
				Object.keys(data).forEach((key) => {
					defineReactive(data, key, data[key], dep);
				});
			};
			//爲該對象設置屬性,添加getter,setter,訂閱者通知
			function defineReactive(data, key, val, dep) {
				// 監聽子屬性
				observe(val);
				//設置訪問器及屬性
				Object.defineProperty(data, key, {
					// 可枚舉
					enumerable: true,
					// 不能再define
					configurable: false,
					//設置getter
					get: () => {
						return val;
					},
					//設置setter
					set: (newVal) => {
						//console.log(val, '=>', newVal);
						//更新值
						val = newVal;
						// 通知所有訂閱者
						dep.notify();
					}
				});
			}
			//訂閱器
			function dep() {
				//訂閱數組
				this.subs = [];
				//添加訂閱
				this.addSub = (sub) => {
					this.subs.push(sub);
				};
				//刪除訂閱
				this.delSub = (key) => {
					// 	//delete sub
				};
				//觸發回調,通知所有訂閱者,觸發update()
				this.notify = () => {
					this.subs.forEach((sub) => {
						sub.update();
					});
				}
			};
			
			let dep = new Dep();
			observe(vm.data, dep);
3,html指令解析與屬性處理

在html部分中,我們在元素標籤上使用的自定義屬性“data”與“@click”,下面的代碼將演示如何處理自定義屬性

要想讀取每個元素的屬性,首先必須獲取所有網頁元素,然後再遍歷每個元素的屬性。而dom對象的children屬性會方便地告知你當前元素下有多少子元素。

//循環獲取所有dom樹
function mvvm(vm) {
	//綁定域 獲取主元素
	let el = document.getElementById(vm.el);
	//循環所有網頁元素
	function eachElDo(el, vm) {
		//處理data屬性
		attrHandler_data(el, vm);
		//處理事件方法
		attrHandler_event(el, vm)
		for (var i = 0; i < el.children.length; i++) {
			eachElDo(el.children[i], vm);
		}
	}
	eachElDo(el, vm)
}

遍歷該元素下的所有屬性,處理自定義屬性。值得注意的是不同網頁標籤取值方式不同。比如p的內容是在標籤內的,而獲取input內容則需要使用value,由於div是佈局元素,所以即使裏面綁定了data也不會被顯示出來。判定標籤使用的是.localName。不是應該使用tagName麼?localname默認都是小寫,我也就直接拿來用了,,,

//處理data屬性,並綁定數據
function attrHandler_data(el, vm) {
	if (el.getAttribute("data")) {
		//回調方法,設定發生數據更新時的動作
		let action = {
			update: () => {	
				//設定不同網頁元素標籤,使用不同的取值方式。		
				switch (el.localName) {
					case "input":
						el.value = vm.data[el.getAttribute("data")];
						break;
					case "div":
						break;
					default:
						el.innerText = vm.data[el.getAttribute("data")];
				}
			}
		}
		//輸入(事件)綁定  爲綁定了data的元素添加事件。
		switch (el.localName) {
			case "input":
				el.addEventListener('input', () => {
					vm.data[el.getAttribute("data")] = el.value;
				})
				break;
			case "div":
				break;
			default:
				el.addEventListener('onchange', () => {
					vm.data[el.getAttribute("data")] = el.innerText;
				})
		}
		//添加觀察者
		dep.addSub(action);
	}
}

解析事件指令
這裏需要注意的是我們的指令是“@”+事件名。事件名就是html默認的時間click、onchange、touchover等等。寫法是<input @click=“go” /> 。我們需要先把@與click分離,再綁定到事件方法adEventListener上。

			function attrHandler_event(el, vm) {
				//遍歷元素所有屬性
				for (var i = 0; i < el.attributes.length; i++) {
					//是否有事件屬性
					if (/@/i.test(el.attributes[i].nodeName)) {
						//用正則分離"@"與事件名
						let myEvent = el.attributes[i].nodeName.replace("@", "");
						//綁定事件
						el.addEventListener(myEvent, () => {
							//事件回調方法
							vm.action[el.getAttribute('@' + myEvent)]();
						}, false)
					}
				}
			}

到此爲止,100行就實現了類vue的mvvm框架。
準備繼續更新一些新的功能,有興趣的同學可以 git:https://github.com/155366311/mvvm

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章