请严格把这个问题当作教育问题来对待。我仍然有兴趣听到新的答案和想法来实现这一点

博士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”这种形式的答案。最理想的情况是,我希望未来不知道如何使用许多框架的读者能够自己掌握如何实现双向数据绑定。我不期待一个完整的答案,但希望能让人理解。


当前回答

迟来的派对,特别是因为我在几个月/几年前写了两篇相关的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中(当然是开放的),以避免留下未绑定的黑洞。

具体细节还可以进一步列出,但在我看来,这些是实现数据绑定的主要原则,一方面是功能的完整性,另一方面是简单性。

因此,除了上面提到的对象观察器之外,我实际上还编写了数据层库,它按照上面提到的概念实现了数据绑定。

其他回答

对象绑定是如何工作的? 倾听形式上的变化是如何起作用的?

更新两个对象的抽象

我认为还有其他技术,但最终我将拥有一个对象,该对象保存对相关DOM元素的引用,并提供一个接口,以协调对其自身数据及其相关元素的更新。

addeventlistener()为此提供了一个非常好的接口。您可以给它一个实现eventListener接口的对象,它将用该对象作为this值调用它的处理程序。

这使您可以自动访问元素及其相关数据。

定义对象

原型继承是实现这一点的好方法,当然不是必需的。首先,创建一个构造函数来接收元素和一些初始数据。

function MyCtor(element, data) {
    this.data = data;
    this.element = element;
    element.value = data;
    element.addEventListener("change", this, false);
}

因此,这里构造函数将元素和数据存储在新对象的属性上。它还将更改事件绑定到给定元素。有趣的是,它将new对象而不是函数作为第二个参数传递。但单靠这个是不行的。

实现eventListener接口

要做到这一点,您的对象需要实现eventListener接口。要做到这一点,只需给对象一个handleEvent()方法。

这就是继承的作用。

MyCtor.prototype.handleEvent = function(event) {
    switch (event.type) {
        case "change": this.change(this.element.value);
    }
};

MyCtor.prototype.change = function(value) {
    this.data = value;
    this.element.value = value;
};

有许多不同的方法可以进行结构化,但是对于协调更新的示例,我决定让change()方法只接受一个值,并让handleEvent传递该值而不是事件对象。这样也可以在没有事件的情况下调用change()。

现在,当change事件发生时,它会更新元素和。data属性。在JavaScript程序中调用.change()时也会发生同样的情况。

使用代码

现在只需创建新对象,并让它执行更新。JS代码中的更新将出现在输入中,并且输入中的更改事件将对JS代码可见。

var obj = new MyCtor(document.getElementById("foo"), "20");

// simulate some JS based changes.
var i = 0;
setInterval(function() {
    obj.change(parseInt(obj.element.value) + ++i);
}, 3000);

演示:http://jsfiddle.net/RkTMD/

改变元素的值可以触发DOM事件。可以使用响应事件的侦听器在JavaScript中实现数据绑定。

例如:

function bindValues(id1, id2) {
  const e1 = document.getElementById(id1);
  const e2 = document.getElementById(id2);
  e1.addEventListener('input', function(event) {
    e2.value = event.target.value;
  });
  e2.addEventListener('input', function(event) {
    e1.value = event.target.value;
  });
}

下面的代码和演示演示了DOM元素如何相互绑定或与JavaScript对象绑定。

绑定任何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);

将变量绑定到输入的一个简单方法(双向绑定)是直接访问getter和setter中的输入元素:

var variable = function(element){                    
                   return {
                       get : function () { return element.value;},
                       set : function (value) { element.value = value;} 
                   }
               };

在HTML中:

<input id="an-input" />
<input id="another-input" />

并使用:

var myVar = new variable(document.getElementById("an-input"));
myVar.set(10);

// and another example:
var myVar2 = new variable(document.getElementById("another-input"));
myVar.set(myVar2.get());

一个不需要getter/setter的更好的方法:

var variable = function(element){

                return function () {
                    if(arguments.length > 0)                        
                        element.value = arguments[0];                                           

                    else return element.value;                                                  
                }

        }

使用方法:

var v1 = new variable(document.getElementById("an-input"));
v1(10); // sets value to 20.
console.log(v1()); // reads value.

我认为我的答案会更有技术含量,但不会像其他人用不同的技术呈现同样的东西那样不同。 所以,首先,这个问题的解决方案是使用一种被称为“观察者”的设计模式,它让你把数据从你的表示中分离出来,使一个东西的变化被广播给它们的侦听器,但在这种情况下,它是双向的。

对于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);

希望我能帮上忙。