JavaScript 实现简单的双向数据绑定(译)

2016/03/02 jqueryangular

双向数据绑定指的就是,绑定对象属性的改变到用户界面的变化的能力,反之亦然。换种说法,如果我们有一个 user 对象和一个 name 属性,一旦我们赋了一个新值给 user.name,在 UI 上就会显示新的姓名了。同样地,如果 UI 包含了一个输入用户姓名的输入框,输入一个新值就应该会使 user 对象的 name 属性做出相应的改变。

很多热门的 JS 框架客户端如 Ember.js,Angular.js 或者 KnockoutJS,都在最新特性上刊登了双向数据绑定。这并不意味着从零实现它很难,也不是说需要这些功能的时候,采用这些框架是唯一的选择。下面的想法实际上很基础,可以被认为是3步走计划:

  1. 我们需要一个 UI 元素和属性相互绑定的方法
  2. 我们需要监视属性和 UI 元素的变化
  3. 我们需要让所有绑定的对象和元素都能感知到变化

还是有很多方法能够实现上面的想法,有一个简单有效的方法就是使用 PubSub 模式。 这个思路很简单:我们使用数据特性来为 HTML 代码进行绑定,所有被绑定在一起的 JavaScript 对象和 DOM 元素都会订阅一个 PubSub 对象。只要 JavaScript 对象或者一个 HTML 输入元素监听到数据的变化时,就会触发绑定到 PubSub 对象上的事件,从而其他绑定的对象和元素都会做出相应的变化。

# 用 jQuery 做一个简单的实现

对于DOM事件的订阅和发布,用 jQuery 实现起来是非常简单的,接下来我们就是用 jQuery 比如下面:

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

对于上面这个实现来说,下面是一个 User 模型的最简单的实现方法:

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

现在我们如果想要将 User 模型属性绑定到UI上,我们只需要将适合的数据特性绑定到对应的 HTML 元素上。

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

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

# 不需要 jQuery 的实现

在如今的大多数项目里,可能已经使用了 jQuery,因此上面的例子完全可以接受。不过,如果我们需要试着向另一个极端做,并且还删除对 jQuery 的依赖,那么怎么做呢?好,证实一下这么做并不难(尤其是在我们限制只支持 IE8 及以上版本的情况下)。最终,我们必须使用一般的 javascript 实现一个定制的 PubSub 并且保留了 DOM 事件:

function DataBinder(object_id) {
    // Create a simple PubSub object
    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(evt) {
            var target = evt.target || evt.srcElement,
            // IE8 compatibility
            prop_name = target.getAttribute(data_attr);

            if (prop_name && prop_name !== "") {
                pubSub.publish(message, prop_name, target.value);
            }
        };

    // Listen to change events and proxy to PubSub
    if (document.addEventListener) {
        document.addEventListener("change", changeHandler, false);
    } else {
        // IE8 uses attachEvent instead of addEventListener
        document.attachEvent("onchange", changeHandler);
    }

    // PubSub propagates changes to all bound elements
    pubSub.on(message, function(evt, 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;
}

除了设置器里调用 jQuery 的 trigger 方法外,模型可以保持一样。调用 trigger 方法将替代为调用我们定制的具有不同特征的 PubSub 的 publish 方法:

// In the model's setter:
function User(uid) {
    // ...
    user = {
        // ...
        set: function(attr_name, val) {
            this.attributes[attr_name] = val;
            // Use the `publish` method
            binder.publish(uid + ":change", attr_name, val, this);
        }
    }

    // ...
}

再次说明一下,我们用一般的纯 javascript 的少于 100 行的维护代码获得了同样的结果。

英文原文:Easy Two-Way Data Binding in JavaScript (opens new window) 本文转载自:http://www.oschina.net/translate/easy-two-way-data-binding-in-javascript (opens new window)

上次更新: 2024/4/15 02:28:03