请严格把这个问题当作教育问题来对待。我仍然有兴趣听到新的答案和想法来实现这一点
博士tl;
如何用JavaScript实现双向数据绑定?
到DOM的数据绑定
通过数据绑定到DOM,我的意思是,例如,拥有一个带有属性b的JavaScript对象a。然后有一个<input> DOM元素(例如),当DOM元素改变时,a也会改变,反之亦然(也就是说,我的意思是双向数据绑定)。
下面是AngularJS的一个图:
JavaScript是这样的:
var a = {b:3};
然后输入(或其他形式)元素,如:
<input type='text' value=''>
我希望输入的值是a.b的值(例如),当输入文本发生变化时,我希望a.b也发生变化。当JavaScript中的a.b发生变化时,输入也会发生变化。
这个问题
在纯JavaScript中完成这个任务的基本技术是什么?
具体来说,我想要一个好的答案参考:
对象绑定是如何工作的?
倾听形式上的变化是如何起作用的?
是否有可能以一种简单的方式只在模板级别修改HTML ?我不想在HTML文档本身中跟踪绑定,而只在JavaScript中跟踪绑定(使用DOM事件,JavaScript保持对所使用的DOM元素的引用)。
我都试过什么?
我是一个大粉丝的胡子,所以我尝试使用它的模板。然而,我在尝试执行数据绑定本身时遇到了问题,因为Mustache将HTML处理为字符串,所以在我得到它的结果后,我没有引用到我的视图模型中的对象在哪里。我能想到的唯一解决办法是用属性修改HTML字符串(或创建DOM树)本身。我不介意使用不同的模板引擎。
基本上,我有一种强烈的感觉,我把手头的问题复杂化了,有一个简单的解决办法。
注意:请不要提供使用外部库的答案,特别是那些有数千行代码的答案。我用过(而且喜欢!)AngularJS和KnockoutJS。我真的不想要“使用框架x”这种形式的答案。最理想的情况是,我希望未来不知道如何使用许多框架的读者能够自己掌握如何实现双向数据绑定。我不期待一个完整的答案,但希望能让人理解。
我认为我的答案会更有技术含量,但不会像其他人用不同的技术呈现同样的东西那样不同。
所以,首先,这个问题的解决方案是使用一种被称为“观察者”的设计模式,它让你把数据从你的表示中分离出来,使一个东西的变化被广播给它们的侦听器,但在这种情况下,它是双向的。
对于DOM到JS的方式
为了将DOM中的数据绑定到js对象,你可以添加数据属性形式的标记(如果你需要兼容性,也可以添加类),像这样:
<input type="text" data-object="a" data-property="b" id="b" class="bind" value=""/>
<input type="text" data-object="a" data-property="c" id="c" class="bind" value=""/>
<input type="text" data-object="d" data-property="e" id="e" class="bind" value=""/>
这样,它可以通过js使用querySelectorAll(或老朋友getElementsByClassName兼容)访问。
现在,您可以将事件监听更改的方式绑定在一起:每个对象一个监听器,或者容器/文档一个大监听器。绑定到文档/容器将为它或它的子容器中的每一个更改触发事件,它将占用更小的内存,但会产生事件调用。
代码看起来像这样:
//Bind to each element
var elements = document.querySelectorAll('input[data-property]');
function toJS(){
//Assuming `a` is in scope of the document
var obj = document[this.data.object];
obj[this.data.property] = this.value;
}
elements.forEach(function(el){
el.addEventListener('change', toJS, false);
}
//Bind to document
function toJS2(){
if (this.data && this.data.object) {
//Again, assuming `a` is in document's scope
var obj = document[this.data.object];
obj[this.data.property] = this.value;
}
}
document.addEventListener('change', toJS2, false);
对于JS做DOM的方式
你需要两件事:一个元对象,它将持有DOM元素的引用,绑定到每个js对象/属性,以及一种方法来监听对象的变化。基本上是相同的方式:您必须有一种方法来侦听对象中的更改,然后将其绑定到DOM节点,因为您的对象“不能有”元数据,您将需要另一个对象以属性名称映射到元数据对象的属性的方式保存元数据。
代码是这样的:
var a = {
b: 'foo',
c: 'bar'
},
d = {
e: 'baz'
},
metadata = {
b: 'b',
c: 'c',
e: 'e'
};
function toDOM(changes){
//changes is an array of objects changed and what happened
//for now i'd recommend a polyfill as this syntax is still a proposal
changes.forEach(function(change){
var element = document.getElementById(metadata[change.name]);
element.value = change.object[change.name];
});
}
//Side note: you can also use currying to fix the second argument of the function (the toDOM method)
Object.observe(a, toDOM);
Object.observe(d, toDOM);
希望我能帮上忙。
迟来的派对,特别是因为我在几个月/几年前写了两篇相关的lib,我以后会提到它们,但看起来仍然与我有关。简短地说,我选择的技术是:
模型观测的代理
用于跟踪DOM更改的MutationObserver(用于绑定原因,而不是值更改)
值的变化(从视图到模型流)通过常规的addEventListener处理程序来处理
恕我直言,除了OP之外,重要的是数据绑定实现将:
处理不同的应用生命周期情况(HTML先,然后JS, JS先,然后HTML,动态属性变化等)
允许模型深度绑定,这样就可以绑定user.address.block
数组作为模型应该得到正确的支持(移位、拼接等)
处理ShadowDOM
尽量简化技术替换,因此任何模板子语言都是一种不适合未来更改的方法,因为它与框架耦合得太紧密了
考虑到所有这些因素,在我看来不可能仅仅抛出几十行JS代码。我试着把它作为一种模式而不是自由——对我没用。
接下来,拥有Object。observe被删除了,但是考虑到模型的观察是至关重要的部分——这整个部分必须被关注——分离到另一个库中。现在说到我如何处理这个问题的原则——正如OP所问的那样:
模型(JS部分)
我对模型观察的看法是代理,恕我直言,这是唯一可行的方法。
功能齐全的观察者应该有自己的库,所以我开发了对象观察者库。
模型应该通过一些专用的API注册,这就是pojo变成可观察对象的地方,在这里看不到任何快捷方式。DOM元素被认为是绑定视图(见下文),首先用模型的值更新,然后在每次数据更改时更新。
视图(HTML部分)
恕我直言,表达绑定最简洁的方式是通过属性。很多人以前这样做过,很多人以后也会这样做,所以这里没有新闻,这是一种正确的方法。在我的例子中,我使用了以下语法:<span data-tie="modelKey:path.to。data => targerProperty"></span>,但这并不重要。对我来说重要的是,HTML中没有复杂的脚本语法——这是错误的,再说一次,恕我直言。
首先要收集指定为绑定视图的所有元素。在我看来,从性能角度来看,管理模型和视图之间的一些内部映射是不可避免的,似乎是牺牲内存+一些管理来节省运行时查找和更新的正确情况。
视图首先从模型中更新(如果可用的话),然后根据后来的模型更改进行更新,正如我们所说的。
而且,应该通过MutationObserver观察整个DOM,以便对动态添加/删除/更改的元素做出反应(绑定/取消绑定)。
此外,所有这些都应该复制到ShadowDOM中(当然是开放的),以避免留下未绑定的黑洞。
具体细节还可以进一步列出,但在我看来,这些是实现数据绑定的主要原则,一方面是功能的完整性,另一方面是简单性。
因此,除了上面提到的对象观察器之外,我实际上还编写了数据层库,它按照上面提到的概念实现了数据绑定。
下面是一个使用Object.defineProperty的想法,它直接修改了访问属性的方式。
代码:
function bind(base, el, varname) {
Object.defineProperty(base, varname, {
get: () => {
return el.value;
},
set: (value) => {
el.value = value;
}
})
}
用法:
var p = new some_class();
bind(p,document.getElementById("someID"),'variable');
p.variable="yes"
小提琴:
所以,我决定把我自己的溶液扔进锅里。这是一个工作的小提琴。注意,这只在非常现代的浏览器上运行。
它使用什么
这个实现非常现代——它需要一个(非常)现代的浏览器和用户两项新技术:
mutationobserver用于检测dom中的变化(事件监听器也被使用)
对象。观察以检测对象中的变化并通知dom。危险,由于这个答案已经被ECMAScript TC讨论并决定反对,考虑一个polyfill。
它是如何工作的
在元素上,放置一个domAttribute:objAttribute映射——例如bind='textContent:name'
在dataBind函数中读取它。观察元素和对象的变化。
当发生更改时,更新相关元素。
解决方案
下面是dataBind函数,注意它只有20行代码,可以更短:
function dataBind(domElement, obj) {
var bind = domElement.getAttribute("bind").split(":");
var domAttr = bind[0].trim(); // the attribute on the DOM element
var itemAttr = bind[1].trim(); // the attribute the object
// when the object changes - update the DOM
Object.observe(obj, function (change) {
domElement[domAttr] = obj[itemAttr];
});
// when the dom changes - update the object
new MutationObserver(updateObj).observe(domElement, {
attributes: true,
childList: true,
characterData: true
});
domElement.addEventListener("keyup", updateObj);
domElement.addEventListener("click",updateObj);
function updateObj(){
obj[itemAttr] = domElement[domAttr];
}
// start the cycle by taking the attribute from the object and updating it.
domElement[domAttr] = obj[itemAttr];
}
下面是一些用法:
HTML:
<div id='projection' bind='textContent:name'></div>
<input type='text' id='textView' bind='value:name' />
JavaScript:
var obj = {
name: "Benjamin"
};
var el = document.getElementById("textView");
dataBind(el, obj);
var field = document.getElementById("projection");
dataBind(field,obj);
这是一把能用的小提琴。注意,这个解决方案是相当通用的。对象。可以使用观察和突变观察器shimming。
绑定任何html输入
<input id="element-to-bind" type="text">
定义两个函数:
function bindValue(objectToBind) {
var elemToBind = document.getElementById(objectToBind.id)
elemToBind.addEventListener("change", function() {
objectToBind.value = this.value;
})
}
function proxify(id) {
var handler = {
set: function(target, key, value, receiver) {
target[key] = value;
document.getElementById(target.id).value = value;
return Reflect.set(target, key, value);
},
}
return new Proxy({id: id}, handler);
}
使用函数:
var myObject = proxify('element-to-bind')
bindValue(myObject);