简单的双向数据绑定

Angular是一款较为出名的前端框架,12bet,的很多特性都是相当酷的,例如:双向数据绑定、依赖注入、指令、MVC(或者说MVVM)、模板(有点像handlebar)。但是被讨论最多的还是双向数据绑定,12bet,其核心思想就是观察者模式(或者说发布订阅模式),本文的重点就在于讨论如何实现观察者模式,12博网址,进而讨论如何实现简单的双向数据绑定。

首先我们先来看看观察者模式:

观察者模式

12博网址,JavaScript是一个事件驱动型语言,观察者模式可谓随处可见,12bet,例如常用的一些onclickattachEventaddEventListener 都是观察者模式的具体应用。

对于DOM的事件操作我们直接使用实现好的就行,但是对于自己实现的对象,12bet,就需要自己实现发布/订阅模型了。这里我们给出一个最简单的例子(更多内容可以参考JavaScript设计模式-观察者模式)。

function PubSub(){  
    var pubSub = {
        callbacks: {},
        on: function(msg,callback) {
            this.callbacks[msg] = this.callbacks[msg] || [];
            this.callbacks[msg].push(callback);
        },
        publish: function(msg) {
            this.callbacks[msg] = this.callbacks[msg] || [];
            for (var i = 0,len = this.callbacks[msg].length; i < len; i++) {
                this.callbacks[msg][i].apply(this,arguments);
            };
        }
      }
      return pubSub;
}
var writer = new PubSub();  
var reader = {  
    read: function(){alert("我读了这本新书")}
};
writer.on("newbook", function(){reader.read()});  
writer.publish("newbook");  

这样我们就完成了一个简单的例子,当作者发布新书之后,用户就可以阅读书籍了。

在以上基础上,我们进一步来讨论双向数据绑定:

双向数据绑定

双向数据绑定指的是将对象属性变化绑定到UI,或者反之。换句话说,如果我们有一个拥有name属性的user对象,当我们给user.name赋予一个新值是UI也会相应的显示新的名字。同样的,如果UI包括了一个输入字段用来输入用户名,输入一个新的值会导致user对象中的那么属性发生变化。双向数据绑定的示意图如下图所示:

two way data binding

许多流行的客户端JavaScript框架例如Ember.js,AngularJS以及KnockoutJS都将双向数据绑定作为自己的头号特性。但是这并不意味着从零开始实现双向数据绑定就很困难,同样的当我们需要双向数据绑定时并不是只能够选择这些框架其中的一个。双向数据绑定底层的思想非常的基本,它可以被压缩成为三个步骤:

  1. 我们需要一个方法来识别哪个UI元素被绑定了相应的属性
  2. 我们需要监视属性和UI元素的变化
  3. 我们需要将所有变化传播到绑定的对象和元素

虽然实现的方法有很多,但是最简单也是最有效的途径是使用发布者-订阅者模式。思想很简单:我们可以使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。

在这里很多人自然会想到使用jQuery,使用DOM的事件操作来监听UI变化,然后将修改对应的数据字段,再使用on来自定义事件监听数据字段的变化,将变化广播到所有绑定的对象和元素上。本文主要讨论的是使用Javascript来实现双向数据绑定,如果对jQuery实现感兴趣可的话可以参考-JavaScript实现简单的双向数据绑定。

首先来看效果:

See the Pen 简单的双向数据绑定 by superlin (@superlin) on CodePen.

这里我们只是对于之前的观察者模式的例子稍作修改,加上了对DOM元素的事件监听:

function DataBinder(object_id){  
    // 创建一个简单的pubSub对象
    var pubSub = {
            callbacks: {},
            on: function(msg,callback) {
                this.callbacks[msg] = this.callbacks[msg] || [];
                this.callbacks[msg].push(callback);
            },
            publish: function(msg) {
                this.callbacks[msg] = this.callbacks[msg] || [];
                for (var i = 0,len = this.callbacks[msg].length; i < len; i++) {
                    this.callbacks[msg][i].apply(this,arguments);
                };
            }
        },
        data_attr = "data-bind-" + object_id,
        message   = object_id + ":change",
        changeHandler = function(event) {
            var target    = event.target || event.srcElement, // IE8兼容
                prop_name = target.getAttribute(data_attr);
            if (prop_name && prop_name !== "") {
                pubSub.publish(message,prop_name,target.value);
            }
        };
    // 监听事件变化,并代理到pubSub
    if (document.addEventListener) {
        document.addEventListener("keyup",changeHandler,false);
    } else{
        // IE8使用attachEvent而不是addEventListenter
        document.attachEvent("onkeyup",changeHandler);
    };
    // pubSub将变化传播到所有绑定元素
    pubSub.on(message,function(event,prop_name,new_val){
        var elements = document.querySelectorAll("[" + data_attr + "=" +prop_name + "]"),
            tag_name;
        for (var i = 0,len = elements.length; i < len; i++) {
            tag_name = elements[i].tagName.toLowerCase();
            if (tag_name === "input" || tag_name === "textarea" || tag_name === "select") {
                elements[i].value = new_val;
            } else{
                elements[i].innerHTML = new_val;
            };
        };
    })
    return pubSub;
}

接着定义模型就行了:

function User(uid) {  
    var binder = new DataBinder(uid),
      user   = {
          attribute : {},
          // 属性设置器使用数据绑定器pubSub来发布
          set : function(attr_name,val) {
              this.attribute[attr_name] = val;
              binder.publish(uid + ":change",attr_name,val,this);
          },
          get : function(attr_name) {
              return this.attribute[attr_name];
          },
          _binder : binder
      };
    binder.on(uid + ":change",function(event,attr_name,new_val,initiator) {
        if (initiator !== user) {
            user.set(attr_name,new_val);
        }
    });
    return user;
  }

使用起来就非常简单了,只需要新建模型,通过模型设置字段就行了

var user = new User( 123 );  
user.set( "name", "lwl" );  

这样们就完成了简单的双向数据绑定了,上面的codepen实例中,我们使用了一个input和p,input输入的值会改变对应的数据字段,数据字段的改变同时会广播到所有所有的UI元素上,这样我们就能在p上看到我们在input里面输入的值了,赶快去试试吧。

参考

  • Easy Two-Way Data Binding in JavaScript
  • JavaScript实现简单的双向数据绑定
  • AngularJS的五个超酷特性
  • JavaScript设计模式-观察者模式
Author image
关于 superlin
Beijing, CN 主页
The reason why a great man is great is that he resolves to be a great man.
默认颜色 边栏居左 边栏居右