双向数据绑定是一种机制,它确保当对象属性发生变化时,这些变化会立即反映在用户界面上,反之亦然。例如,若存在一个包含name属性的user对象,当我们修改user.name的值时,界面上显示的名字也会相应更新。同样,如果界面上有一个用于输入用户名的文本框,用户在此输入的新值也会导致user对象的name属性更新。
虽然像Ember.js、Angular.js或KnockoutJS这样的现代Javascript框架提供了内置的双向数据绑定功能,但这并不意味着没有它们就无法实现这一功能。实际上,实现双向数据绑定的基本原理相当简单,可以通过三个步骤来概括:
- 定义一种方式,使UI元素能够与对象属性相绑定;
- 监控这些属性和UI元素的状态变化;
- 确保所有相关联的对象和元素都能接收到并响应这些变化。
一个有效的实现方法是采用发布/订阅(PubSub)模式。在这种模式下,HTML元素通过特定的数据属性与Javascript对象关联,所有相关的对象和DOM元素都订阅同一个PubSub实例。当检测到数据变化时,会触发PubSub实例上的事件,通知所有订阅者进行相应的更新。
以下是使用jQuery简化DOM事件处理的一个示例:
function DataBinder(objectId) {
const pubSub = $( {} );
const dataAttr = `bind-${objectId}`;
const message = `${objectId}:change`;
$(document).on('change', `[data-${dataAttr}]`, function(event) {
const $input = $(this);
pubSub.trigger(message, [$input.data(dataAttr), $input.val()]);
});
pubSub.on(message, function(event, propName, newVal) {
$(`[data-${dataAttr}='${propName}']`).each(function() {
const $bound = $(this);
if ($bound.is('input, textarea, select')) {
$bound.val(newVal);
} else {
$bound.html(newVal);
}
});
});
return pubSub;
}
基于上述DataBinder,我们可以创建一个简单的User模型:
function User(uid) {
const binder = new DataBinder(uid);
const user = {
attributes: {},
set(attrName, val) {
this.attributes[attrName] = val;
binder.trigger(`${uid}:change`, [attrName, val, this]);
},
get(attrName) {
return this.attributes[attrName];
},
_binder: binder
};
binder.on(`${uid}:change`, function(event, attrName, newVal, initiator) {
if (initiator !== user) {
user.set(attrName, newVal);
}
});
return user;
}
为了将User模型的属性绑定到UI上,只需在HTML元素上添加适当的数据属性即可。例如,设置用户名称的输入框如下所示:
此外,也可以不依赖jQuery来实现相同的功能。通过使用原生Javascript编写自定义的PubSub实现,并处理DOM事件,可以达到同样的效果。这种方法虽然稍微复杂一些,但完全可行,尤其适用于那些不想引入额外库的项目。
function DataBinder(objectId) {
const pubSub = {
callbacks: {},
on(msg, callback) {
(this.callbacks[msg] = this.callbacks[msg] || []).push(callback);
},
publish(msg, ...args) {
(this.callbacks[msg] || []).forEach(cb => cb(...args));
}
};
const dataAttr = `data-bind-${objectId}`;
const message = `${objectId}:change`;
const changeHandler = event => {
const target = event.target || event.srcElement;
const propName = target.getAttribute(dataAttr);
if (propName) {
pubSub.publish(message, propName, target.value);
}
};
if (document.addEventListener) {
document.addEventListener('change', changeHandler, false);
} else {
document.attachEvent('onchange', changeHandler);
}
pubSub.on(message, (event, propName, newVal) => {
document.querySelectorAll(`[${dataAttr}='${propName}']`).forEach(element => {
if (['input', 'textarea', 'select'].includes(element.tagName.toLowerCase())) {
element.value = newVal;
} else {
element.innerHTML = newVal;
}
});
});
return pubSub;
}
通过这种方式,即使在没有jQuery的情况下,也能够以简洁且高效的方式实现双向数据绑定。