双向数据绑定简述
双向数据绑定,可以将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="输入任意值"并按回车键运行时,input输入框的值也会跟着变,这就实现了model更改UI的效果~~~
可见,Object.defineProperty()实现双向绑定比发布订阅模式简单得多~~~
脏值检查
是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval()定时轮询检测数据的变动。 脏值检查实现较为复杂,暂时没时间进行研究~~~