谈谈JavaScript中的双向数据绑定
发布在每天学点javascript2013年12月21日view:84337
在文章任何区域双击击即可给文章添加【评注】!浮到评注点上可以查看详情。

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

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

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

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

使用jQuery的简单实现

使用jQuery来实现双向数据绑定非常的直接且简单,因为这个流行的库能够是我们轻松的订阅和发布DOM事件,以及我们自定义的事件:

function DataBinder(object_id){
    //使用一个jQuery对象作为简单的订阅者发布者
    var pubSub = jQuery({});

    //我们希望一个data元素可以在表单中指明绑定:data-bind-<object_id>="<property_name>"        

    var data_attr = "bind-" + object_id,
            message = object_id + ":change";

    //使用data-binding属性和代理来监听那个元素上的变化事件
    // 以便变化能够“广播”到所有的关联对象   

    jQuery(document).on("change","[data-" + data_attr + "]",function(evt){
        var input = jQuery(this);
        pubSub.trigger(message, [ $input.data(data_attr),$input.val()]);
    });

    //PubSub将变化传播到所有的绑定元素,设置input标签的值或者其他标签的HTML内容   

    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,text area,select")){
            $bound.val(new_val);
        }else{
            $bound.html(new_val);
        }
        });
    });

    return pubSub;
}

在这个实验中可以按照以下代码简单的实现一个User模型:

function User(uid){
    var binder = new DataBinder(uid),

        user = {
            atttibutes: {},

            //属性设置器使用数据绑定器PubSub来发布变化   

            set: function(attr_name,val){
                this.attriures[attr_name] = val;
                binder.trigger(uid + ":change", [attr_name, val, this]);
            },

            get: function(attr_name){
                return this.attributes[attr_name];
            },

            _binder: binder
        };

        binder.on(uid +":change",function(vet,attr_name,new_val,initiator){
            if(initiator !== user){
                user.set(attr_name,new_val);
            }
        })
}

现在,无论我们什么时候想把模型的属性绑定到UI的一部分上,我们只需要在相应的HTML元素上设置一个合适的data属性即可。

//JavaScript

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

//html

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

input字段的值会自动反映出user对象的name属性,反之亦然。任务完成了!

不使用jQuery来创建数据双向绑定

在入如今的大多数项目中,都可能已经用到了jQuery,因此完全可以借用前面的例子。但是如果我们更进一步,移除对jQuery的依赖会怎样呢?事实上,这并不是太困难(尤其是当我们限定只支持IE8以上的版本)。最终,我们需要使用原生的JavaScript来实现一个自定义的PubSub以及观察DOM事件。

function DataBinder(object_id){
    //创建一个简单地PubSub对象   

    var pubSub = {
        callbacks: {}.

        on: function(msg,calssback){
            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<lenli++){
                this.callbacks[msg][i].apply(this,arguments);
            }
        }
    },

    data_attr = "data-bind-" + object_id,
    message = object_id + ":change",

    changeHandler = function(evt){
        var target = evt.target || evt.srcElemnt, //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("change",changeHandler,false);
    }else{
        //IE8使用attachEvent而不是addEventListener     
        document.attachEvent("onchange",changeHandler);
    }

    //PubSub将变化传播到所有绑定元素    

    pubSub.on(message,function(vet,prop_name,new)_val){
        var elements = document.querySelectorAll("[" + data_attr + "=" + prop_name + "]"),
                tah_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;
}

模型可以和勤勉你的例子保持一直,除了在设置器中调用那个jQuery的trigger方法之外,它需要通过调用一个自定义的PubSub的publish方法来实现:

//在model的设置器中   

function User(uid){
//...

user = {
//...
set: function(attr_name,val){
    this.attribute[attr_name] = val;
    //使用“publish”方法  
    binder.publish(uid+ ":change", attr_name, val,this);
        }
    }

    //...
}        

再一次,我们使用原生的JavaScript代码实现了相同的结果,而不是使用臃肿的JavaScript框架。


本文译自easy two way data-binding in JavaScript,原文地址http://www.lucaongaro.eu/blog/2012/12/02/easy-two-way-data-binding-in-javascript/

如果你觉得本文对你有帮助,请点击下面的链接为我提供赞助

评论
发表评论
1年前
添加了一枚【评注】:少了个分号
1年前

弄出这种满是错误的教程,真的缺心眼

1年前

1年前
赞了此文章!
1年前
添加了一枚【评注】:这是number,上面set是字符串,怎么会有反应,这些代码中存在那么多问题,有点质疑
1年前
添加了一枚【评注】:这里为什么要重写change事件?
2年前
添加了一枚【评注】:。。。
2年前
添加了一枚【评注】:勉强
2年前
添加了一枚【评注】:name 属性 哈哈哈
2年前

object_id的意思是不是就是类似angular中一个component对应一个controller(或者是scope也可以)

2年前

我只想知道为什么要加一个object_id参数,有什么用

2年前

这笔误也太多了吧

2年前
添加了一枚【评注】:这里应为jQuery(evt.target); jQuery(this)指的是$(document)
2年前
添加了一枚【评注】:tag_name
2年前
添加了一枚【评注】:拼错了吧
2年前
添加了一枚【评注】:这个想法不错
2年前
添加了一枚【评注】:什么时候 触发这个监听事件?
2年前
添加了一枚【评注】:这么好玩
2年前

写的有点问题吧 如果不是input,textarea,select没法触发change事件的~

2年前
添加了一枚【评注】:attributes
2年前
添加了一枚【评注】:evt没用到
2年前
添加了一枚【评注】:少了个$
3年前
添加了一枚【评注】:, 号
3年前
添加了一枚【评注】:少一个;
3年前
添加了一枚【评注】:是 atttibutes
3年前
添加了一枚【评注】:多了一个$
3年前
添加了一枚【评注】:多了一个$
3年前
添加了一枚【评注】:应该是atttibutes
3年前
添加了一枚【评注】:多了个 )
3年前
添加了一枚【评注】:少了个;
3年前
添加了一枚【评注】:callback
4年前
添加了一枚【评注】:一致
4年前
添加了一枚【评注】:最后少了个return user;
4年前
添加了一枚【评注】:这里是textarea
4年前
添加了一枚【评注】:少了个$
5年前
赞了此文章!
5年前

@我就看吓不说话 呵呵呵。这水平,是真的菜鸟

5年前

感谢翻译,但是代码拷过来时候似乎有错误。。。jquery版那个没有return user 还有$input…少了$

5年前

document 上有 change 事件?

5年前

@刘_ocean AngularJS的双向绑定体现在ng-model指令的使用上,在compile phase阶段,AngularJS会将模板编译为一个link函数,并将其link到相应的scope,往后再通过digest循环中的dirty check对视图和模型内容进行更新。AngularJS的模板和以往使用的Handlerbars等模板不一样,只要进行一次compile之后便可以自行实现数据和视图的同步,不需要手动触发事件。具体的原理可以参考《build your own AngularJS》一书或者AngularJS文档。希望我的回答对你有所帮助。

5年前

那么~ angular的$scope.data = ‘newvalue’;,这种赋值怎么trigger双向绑定的呢?

WRITTEN BY
张小俊128
Intern in Baidu mobile search department。认真工作,努力钻研,期待未来更多可能。
TA的新浪微博
PUBLISHED IN
每天学点javascript

javascript进阶级教程,循序渐进掌握javascript

我的收藏