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

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


当前回答

我想给我的预习添点什么。我建议使用一种稍微不同的方法,它允许您不使用方法而简单地为对象分配一个新值。但必须指出的是,特别是老版本的浏览器不支持这一点,IE9仍然需要使用不同的界面。

最值得注意的是,我的方法没有使用事件。

getter和setter

我的建议利用了getter和setter相对较年轻的特性,特别是仅使用setter。一般来说,突变器允许我们“自定义”某些属性如何赋值和检索的行为。

这里我将使用的一个实现是Object.defineProperty方法。它适用于火狐,谷歌浏览器,我想还有IE9。还没有测试其他浏览器,但由于这只是理论…

不管怎样,它接受三个参数。第一个参数是您希望为其定义新属性的对象,第二个参数是类似于新属性名称的字符串,最后一个“描述符对象”提供关于新属性行为的信息。

两个特别有趣的描述符是get和set。示例如下所示。注意,使用这两个描述符将禁止使用其他4个描述符。

function MyCtor( bindTo ) {
    // I'll omit parameter validation here.

    Object.defineProperty(this, 'value', {
        enumerable: true,
        get : function ( ) {
            return bindTo.value;
        },
        set : function ( val ) {
            bindTo.value = val;
        }
    });
}

现在使用这个有点不同:

var obj = new MyCtor(document.getElementById('foo')),
    i = 0;
setInterval(function() {
    obj.value += ++i;
}, 3000);

我想强调的是,这只适用于现代浏览器。

工作提琴:http://jsfiddle.net/Derija93/RkTMD/1/

其他回答

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

更新两个对象的抽象

我认为还有其他技术,但最终我将拥有一个对象,该对象保存对相关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/

在这个链接“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>

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

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

希望我能帮上忙。

在过去的7年里,情况发生了很大的变化,我们现在在大多数浏览器中都有本地web组件。在我看来,问题的核心是在元素之间共享状态,一旦你有了它,当状态改变时更新ui就很简单了,反之亦然。

为了在元素之间共享数据,你可以创建一个StateObserver类,并以此扩展你的web组件。一个最小的实现是这样的:

// create a base class to handle state class StateObserver extends HTMLElement { constructor () { super() StateObserver.instances.push(this) } stateUpdate (update) { StateObserver.lastState = StateObserver.state StateObserver.state = update StateObserver.instances.forEach((i) => { if (!i.onStateUpdate) return i.onStateUpdate(update, StateObserver.lastState) }) } } StateObserver.instances = [] StateObserver.state = {} StateObserver.lastState = {} // create a web component which will react to state changes class CustomReactive extends StateObserver { onStateUpdate (state, lastState) { if (state.someProp === lastState.someProp) return this.innerHTML = `input is: ${state.someProp}` } } customElements.define('custom-reactive', CustomReactive) class CustomObserved extends StateObserver { connectedCallback () { this.querySelector('input').addEventListener('input', (e) => { this.stateUpdate({ someProp: e.target.value }) }) } } customElements.define('custom-observed', CustomObserved) <custom-observed> <input> </custom-observed> <br /> <custom-reactive></custom-reactive>

小提琴在这里

我喜欢这种方法,因为:

no dom traversal to find data- properties no Object.observe (deprecated) no Proxy (which provides a hook but no communication mechanism anyway) no dependencies, (other than a polyfill depending on your target browsers) it's reasonably centralised & modular... describing state in html, and having listeners everywhere would get messy very quickly. it's extensible. This basic implementation is 20 lines of code, but you could easily build up some convenience, immutability, and state shape magic to make it easier to work with.