雙向資料繫結簡述
雙向資料繫結,可以將JS物件的屬性繫結到DOM節點上,實現JS物件跟DOM節點的同名屬性的關聯,改變一方時,另一方也會得到更新。
雙向資料繫結的思想大致如下: 一、將DOM節點的屬性跟JS物件的屬性建立關聯 二、監聽JS屬性跟DOM元素的變化 三、同時修改JS物件跟DOM元素
常見的實現資料繫結的做法有如下幾種: 一、釋出-訂閱模式(backbone.js) 二、髒值檢查(angular.js) 三、資料劫持(vue.js)
釋出訂閱模式實現
釋出訂閱模式詳見這篇文章,原理是一種一對多的關係,讓多個觀察者物件同時監聽釋出者物件,當釋出者發生改變時,所有觀察者也會得到通知。
實現原理
通過釋出訂閱模式實現資料雙向繫結的原理如下: 一、當model傳送改變時,觸發model change事件,然後通過相應的事件處理函式更新。 二、當介面更新時,觸發UI change事件,然後通過相應的事件處理函式更新model,以及繫結在model上的其他介面控制元件。
依據這個思路,可以定義ui-update-event和model-update-event兩個事件。下面將分別介紹。
具體實現
直接上程式碼~~~
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>釋出訂閱模式實現資料雙向繫結</title>
<style>
#inputId {
border:1px solid #ccc;
width:200px;
height:24px;
}
#modelView {
border:1px solid black;
width:200px;
height:24px;
margin-top:20px;
margin-bottom:20px;
}
</style>
</head>
<body>
<input type="text" id="inputId" d-binding="user.name"/>
<div id="modelView" d-binding="user.name"></div>
<button id="btn">model的變化導致view的變化</button>
<script>
// 釋出訂閱原型
var pubSub = {
allCallbacks: [],
// 增加訂閱者
on: function(eventName, callback) {
// 如果沒有訂閱過該訊息,給這個訊息建立一個快取列表
if(!this.allCallbacks[eventName]) {
this.allCallbacks[eventName] = [];
}
this.allCallbacks[eventName].push(callback);
},
// 釋出訊息
public: function() {
var eventName = Array.prototype.shift.call(arguments);
// 取出該訊息對應的回撥函式集合
var callbacks = this.allCallbacks[eventName];
if (!callbacks || callbacks.length === 0) {
return false;
}
for (var i = 0; i < callbacks.length; i++) {
var callback = callbacks[i];
callback.apply(this, arguments);
}
}
};
var DataBinder = (function () {
function changeHandler(e) {
var target = e.target || e.srcElement;
var attrName = target.getAttribute("d-binding");
if (attrName && attrName !== "") {
// 釋出訊息
pubSub.public("ui-update-event", attrName, target.value);
}
};
// 監聽檢視層的事件變化
if (document.addEventListener) {
document.addEventListener('keyup', changeHandler, false);
document.addEventListener('change', changeHandler, false);
} else {
document.attachEvent("onkeyup", changeHandler);
document.attachEvent("onchange", changeHandler);
}
// 監聽模型上的變化,並把變化傳播到所有繫結的元素上
pubSub.on("model-update-event", function(attrName, newVal) {
var elements = document.querySelectorAll('[d-binding="' + attrName + '"]');
var tagName;
for (var i = 0, ilen = elements.length; i < ilen; i++) {
tagName = elements[i].tagName.toLowerCase();
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
elements[i].value = newVal;
} else {
elements[i].innerHTML = newVal;
}
}
});
return {
modelName : "",
initModel : function (modelName) {
var self = this;
self.modelName = modelName;
pubSub.on("ui-update-event", function(attrName, propValue){
var propPathArr = attrName.split(".");
self.updateModelData(propPathArr[1], propValue);
});
return Object.create(this);
},
loadModelData : function (modelData) {
for (prop in modelData) {
this.updateModelData(prop, modelData[prop]);
}
},
updateModelData : function (propName, propValue) {
eval(this.modelName)[propName] = propValue;
pubSub.public("model-update-event", this.modelName + '.' + propName, propValue);
}
}
})();
var user = DataBinder.initModel("user");
user.loadModelData({
'name' : 1
});
// 測試模型的變化到 檢視層的變化
var btn = document.getElementById("btn");
var inputId = document.getElementById("inputId");
btn.onclick = function() {
var value = inputId.value;
user.updateModelData("name", parseInt(value) + 1);
};
</script>
</body>
</html>
複製程式碼
ui-update-event事件
對於所有支援雙向繫結的頁面控制元件,當值發生改變時,就會觸發ui-update-event事件更新model,以及繫結在model上的其他控制元件。 觸發ui-update-event時,先執行
pubSub.on("ui-update-event", function(attrName, propValue){
var propPathArr = attrName.split(".");
self.updateModelData(propPathArr[1], propValue);
});
複製程式碼
通過updateModelData方法去執行model-update-event,從而更新model。
updateModelData : function (propName, propValue) {
eval(this.modelName)[propName] = propValue;
pubSub.public("model-update-event", this.modelName + '.' + propName, propValue);
複製程式碼
model-update-event
對於model這一層,當model發生改變時,會觸發model-update-event的監聽事件
pubSub.on("model-update-event", function(attrName, newVal) {
var elements = document.querySelectorAll('[d-binding="' + attrName + '"]');
var tagName;
for (var i = 0, ilen = elements.length; i < ilen; i++) {
tagName = elements[i].tagName.toLowerCase();
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
elements[i].value = newVal;
} else {
elements[i].innerHTML = newVal;
}
}
});
複製程式碼
從而修改了DOM元素的值。
屬性劫持
Object.defineProperty()方法直接在物件上定義一個新屬性,或修改物件上的現有屬性,並返回該物件。 關於Object.defineProperty()的介紹如下:
Object.defineProperty(obj, prop, descriptor)
引數
obj:定義屬性的物件
prop:要定義或修改的屬性的名稱。
descriptor:定義或修改屬性的描述符。
返回值:傳遞給函式的物件。
注意:資料描述符和訪問器描述符,不能同時存在(value,writable 和 get,set)
get:函式return將被用作屬性的值。
set:該函式將僅接收引數賦值給該屬性的新值。(在屬性改變時呼叫)
複製程式碼
使用Object.defineProperty()實現雙向資料繫結
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>使用Object.defineProperty實現簡單的雙向資料繫結</title>
</head>
<body>
<input type="text" id="input" />
<div id="div"></div>
<script>
var obj = {};
var inputVal = document.getElementById("input");
var div = document.getElementById("div");
Object.defineProperty(obj, "name", {
set: function(newVal) {
inputVal.value = newVal;
div.innerHTML = newVal;
}
});
inputVal.addEventListener('input', function(e){
obj.name = e.target.value;
});
</script>
</body>
</html>
複製程式碼
當在input輸入框輸入值的時候,div也會顯示對應的值,實現了UI更改model的效果~~~
當在控制檯輸入 obj.name="輸入任意值"並按Enter鍵執行時,input輸入框的值也會跟著變,這就實現了model更改UI的效果~~~
可見,Object.defineProperty()實現雙向繫結比釋出訂閱模式簡單得多~~~
髒值檢查
是通過髒值檢測的方式比對資料是否有變更,來決定是否更新檢視,最簡單的方式就是通過 setInterval()定時輪詢檢測資料的變動。 髒值檢查實現較為複雜,暫時沒時間進行研究~~~