一,基礎知識
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