双向数据绑定指的就是,绑定对象属性的改变到用户界面的变化的能力,反之亦然。换种说法,如果我们有一个 user 对象和一个 name 属性,一旦我们赋了一个新值给 user.name,在 UI 上就会显示新的姓名了。同样地,如果 UI 包含了一个输入用户姓名的输入框,输入一个新值就应该会使 user 对象的 name 属性做出相应的改变。
很多热门的 JS 框架客户端如 Ember.js,Angular.js 或者 KnockoutJS,都在最新特性上刊登了双向数据绑定。这并不意味着从零实现它很难,也不是说需要这些功能的时候,采用这些框架是唯一的选择。下面的想法实际上很基础,可以被认为是3步走计划:
- 我们需要一个 UI 元素和属性相互绑定的方法
- 我们需要监视属性和 UI 元素的变化
- 我们需要让所有绑定的对象和元素都能感知到变化
还是有很多方法能够实现上面的想法,有一个简单有效的方法就是使用 PubSub 模式。 这个思路很简单:我们使用数据特性来为 HTML 代码进行绑定,所有被绑定在一起的 JavaScript 对象和 DOM 元素都会订阅一个 PubSub 对象。只要 JavaScript 对象或者一个 HTML 输入元素监听到数据的变化时,就会触发绑定到 PubSub 对象上的事件,从而其他绑定的对象和元素都会做出相应的变化。
# 用 jQuery 做一个简单的实现
对于DOM事件的订阅和发布,用 jQuery 实现起来是非常简单的,接下来我们就是用 jQuery 比如下面:
function DataBinder(object_id) {
// Use a jQuery object as simple PubSub
var pubSub = jQuery({});
// We expect a `data` element specifying the binding
// in the form: data-bind-<object_id>="<property_name>"
var data_attr = "bind-" + object_id,
message = object_id + ":change";
// Listen to change events on elements with the data-binding attribute and proxy
// them to the PubSub, so that the change is "broadcasted" to all connected objects
jQuery(document).on("change", "[data-" + data_attr + "]", function(evt) {
var $input = jQuery(this);
pubSub.trigger(message, [$input.data(data_attr), $input.val()]);
});
// PubSub propagates changes to all bound elements, setting value of
// input tags or HTML content of other tags
pubSub.on(message, function(evt, prop_name, new_val) {
jQuery("[data-" + data_attr + "=" + prop_name + "]").each(function() {
var $bound = jQuery(this);
if ($bound.is("input, textarea, select")) {
$bound.val(new_val);
} else {
$bound.html(new_val);
}
});
});
return pubSub;
}
对于上面这个实现来说,下面是一个 User 模型的最简单的实现方法:
function User(uid) {
var binder = new DataBinder(uid),
user = {
attributes: {},
// The attribute setter publish changes using the DataBinder PubSub
set: function(attr_name, val) {
this.attributes[attr_name] = val;
binder.trigger(uid + ":change", [attr_name, val, this]);
},
get: function(attr_name) {
return this.attributes[attr_name];
},
_binder: binder
};
// Subscribe to the PubSub
binder.on(uid + ":change", function(evt, attr_name, new_val, initiator) {
if (initiator !== user) {
user.set(attr_name, new_val);
}
});
return user;
}
现在我们如果想要将 User 模型属性绑定到UI上,我们只需要将适合的数据特性绑定到对应的 HTML 元素上。
<script>
var user = new User( 123 );
user.set( "name", "Wolfgang" );
</script>
<input type="number" data-bind-123="name" />
# 不需要 jQuery 的实现
在如今的大多数项目里,可能已经使用了 jQuery,因此上面的例子完全可以接受。不过,如果我们需要试着向另一个极端做,并且还删除对 jQuery 的依赖,那么怎么做呢?好,证实一下这么做并不难(尤其是在我们限制只支持 IE8 及以上版本的情况下)。最终,我们必须使用一般的 javascript 实现一个定制的 PubSub 并且保留了 DOM 事件:
function DataBinder(object_id) {
// Create a simple PubSub object
var pubSub = {
callbacks: {},
on: function(msg, callback) {
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 < len; i++) {
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.srcElement,
// IE8 compatibility
prop_name = target.getAttribute(data_attr);
if (prop_name && prop_name !== "") {
pubSub.publish(message, prop_name, target.value);
}
};
// Listen to change events and proxy to PubSub
if (document.addEventListener) {
document.addEventListener("change", changeHandler, false);
} else {
// IE8 uses attachEvent instead of addEventListener
document.attachEvent("onchange", changeHandler);
}
// PubSub propagates changes to all bound elements
pubSub.on(message, function(evt, prop_name, new_val) {
var elements = document.querySelectorAll("[" + data_attr + "=" + prop_name + "]"),
tag_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 方法外,模型可以保持一样。调用 trigger 方法将替代为调用我们定制的具有不同特征的 PubSub 的 publish 方法:
// In the model's setter:
function User(uid) {
// ...
user = {
// ...
set: function(attr_name, val) {
this.attributes[attr_name] = val;
// Use the `publish` method
binder.publish(uid + ":change", attr_name, val, this);
}
}
// ...
}
再次说明一下,我们用一般的纯 javascript 的少于 100 行的维护代码获得了同样的结果。
英文原文:Easy Two-Way Data Binding in JavaScript (opens new window) 本文转载自:http://www.oschina.net/translate/easy-two-way-data-binding-in-javascript (opens new window)