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

博士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事件。可以使用响应事件的侦听器在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对象绑定。

其他回答

在这个链接“Easy Two-Way Data Binding in JavaScript”中有一个非常简单的双向数据绑定实现。

之前的链接以及来自knockoutjs, backbone.js和agility.js的想法,导致了这个轻量级和快速的MVVM框架,ModelView.js基于jQuery,它很好地使用jQuery,我是这个框架的谦虚(或者不那么谦虚)的作者。

复制下面的示例代码(来自博客文章链接):

DataBinder的示例代码

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;
}

对于JavaScript对象而言,是一个最小的实现 本实验的用户模型如下:

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;
}

现在,每当我们想要将一个模型的属性绑定到一个UI块时,我们 只需要设置相应的数据属性 HTML元素:

// javascript
var user = new User( 123 );
user.set( "name", "Wolfgang" );

<!-- html -->
<input type="number" data-bind-123="name" />

我已经通过一些基本的javascript示例,使用onkeypress和onchange事件处理程序,使绑定视图到我们的js和js视图

这里的例子plunker http://plnkr.co/edit/7hSOIFRTvqLAvdZT4Bcc?p=preview

<!DOCTYPE html>
<html>
<body>

    <p>Two way binding data.</p>

    <p>Binding data from  view to JS</p>

    <input type="text" onkeypress="myFunction()" id="myinput">
    <p id="myid"></p>
    <p>Binding data from  js to view</p>
    <input type="text" id="myid2" onkeypress="myFunction1()" oninput="myFunction1()">
    <p id="myid3" onkeypress="myFunction1()" id="myinput" oninput="myFunction1()"></p>

    <script>

        document.getElementById('myid2').value="myvalue from script";
        document.getElementById('myid3').innerHTML="myvalue from script";
        function myFunction() {
            document.getElementById('myid').innerHTML=document.getElementById('myinput').value;
        }
        document.getElementById("myinput").onchange=function(){

            myFunction();

        }
        document.getElementById("myinput").oninput=function(){

            myFunction();

        }

        function myFunction1() {

            document.getElementById('myid3').innerHTML=document.getElementById('myid2').value;
        }
    </script>

</body>
</html>

迟来的派对,特别是因为我在几个月/几年前写了两篇相关的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/

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