双向数据绑定指的是监听数据的变化,将对象属性的变化绑定到UI上。实现双向数据绑定主要是以下三个步骤:
1. 实现一个方法用来识别哪个UI元素被绑定了相应的属性
2. 需要监听属性和UI元素的变化
3. 需要将变化传播到绑定的对象上
实现该功能最典型的做法就是使用:
1. 发布者-订阅者模式
思想很简单:我们可以使用自定义的data属性在HTML代码中指明绑定。所有绑定起>来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。
2. 数据劫持
通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的
先说一下发布-订阅模式,分别用jQuery实现和原生js实现
发布-订阅模式
jQuery实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| function DataBinder(object_id){ var pubSub = $({});
var data_attr = "bind-" + object_id, message = object_id + ":change";
$(document).on("change","[data-" + data_attr + "]",function(evt){ var input = $(this); pubSub.trigger(message, [ $input.data(data_attr),$input.val()]); });
pubSub.on(message,function(evt,prop_name,new_val){ $("[data-" + data_attr + "=" + prop_name + "]").each(function(){ var $bound = $(this);
if($bound.is("input,text area,select")){ $bound.val(new_val); }else{ $bound.html(new_val); } }); });
return pubSub; }
|
实现一个简单的数据模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| function User(uid){ var binder = new DataBinder(uid),
user = { atttibutes: {},
set: function(attr_name,val){ this.attriures[attr_name] = val; binder.trigger(uid + ":change", [attr_name, val, this]); },
get: function(attr_name){ return this.attributes[attr_name]; },
_binder: binder };
binder.on(uid +":change",function(vet,attr_name,new_val,initiator){ if(initiator !== user){ user.set(attr_name,new_val); } }) }
|
现在,无论我们什么时候想把模型的属性绑定到UI的一部分上,我们只需要在相应的HTML元素上设置一个合适的data属性即可
1 2 3 4 5 6 7 8
|
var user = new User(123); user.set("name","Wolfgang");
<input type="number" data-bind-123="name" />
|
原生js实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| function DataBinder(object_id){
var pubSub = { callbacks: {}.
on: function(msg,calssback){ this.callbacks[msg] = this.callbacks[msg] || []; this.callbacks[msg].push(callback); },
publish: function(msg){ this.callbacks[msg] = this.callbacks[msg] || []; for(var i = 0, len = this.callbacks[msg].length; i<lenli++){ this.callbacks[msg][i].apply(this,arguments); } } },
data_attr = "data-bind-" + object_id, message = object_id + ":change",
changeHandler = function(evt){ var target = evt.target || evt.srcElemnt, prop_name = target.getAttribute(data_attr);
if(prop_name && prop_name !== ""){ pubSub.publish(message,prop_name,target.value); } };
if(document.addEventListener){ document.addEventListener("change",changeHandler,false); }else{ document.attachEvent("onchange",changeHandler); }
pubSub.on(message,function(vet,prop_name,new)_val){ var elements = document.querySelectorAll("[" + data_attr + "=" + prop_name + "]"), tah_name;
for(var i = 0,len =elements.length; i < len; i++){ tag_name = elements[i].tagName.toLowerCase();
if(tag_name === "input" || tag_name === "textarea" || tag_name === "select"){ elements[i].value = new_val; }else{ elements[i].innerHTML = new_val; } } });
return pubSub; }
|
模型可以和前面的例子保持一直,除了在设置器中调用那个jQuery的trigger方法之外,它需要通过调用一个自定义的PubSub的publish方法来实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
function User(uid){
user = {
set: function(attr_name,val){ this.attribute[attr_name] = val; binder.publish(uid+ ":change", attr_name, val,this); } }
}
|
数据劫持
要实现以下几点:
1. 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2. 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3. 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4. mvvm入口函数,整合以上三者