読者です 読者をやめる 読者になる 読者になる

JavaScriptで(Qt|.NETのevent)っぽいSignalSlot作った

JavaScript

UIを実装するとObserverパターンおよびその派生は必須になるので、毎回車輪の再発明をしてるんだけど、個人的にこれでいいんじゃね、的なコードができたので晒しておく。

利用側のコードはこんな感じ。

var ObservableList = function () {
    Signals.define(this, 'onElementAdded', 'onElementRemoved');
    this._list = [];
}
ObservableList.prototype.add = function (value) {
    this._list.push(value);
    this.onElementAdded(value);
    return value;
}
ObservableList.prototype.removeAt = function (index) {
    var removed = this._list.splice(index, 1);
    this.onElementRemoved(removed);
    return removed;
}

// connect signal
var list = new ObservableList();
list.onElementAdded.connect(function (sender, elem) { alert('added:' + elem); });
list.onElementRemoved.connect(function (sender, elem) { alert('removed:' + elem); });

// emit
list.add("fuga");
list.add("hoge");
list.removeAt(0);

定義したSignalがfunctionになっていて、.NETのeventのようにfireが書けるというのがこいつの大きな特徴ですね。emitって書かなくていいQtともいう。
以下、Signals(signals.js)のコード。例のごとく一応MITライセンスにしておく。

/**
 * signals.js
 *
 * @author terurou
 * @license MIT License
 */
(function (window, document) {
    if (!window.Signals) window.Signals = {};
    Signals.define = define;

    if (typeof Array.prototype.forEach !== 'function') {
        Array.prototype.forEach = function (callback, thisObj) {
            for (var i = 0, len = this.length; i < len; ++i) {
                callback.call(thisObj, this[i], i, this);
            }
        }
    }

    function connect(slot, one) {
        var slots = this.__slots;
        slots[slots.length] = [slot, !!one];
    }

    function disconnect(slot) {
        var slots = this.__slots;
        for (var i = slots.length - 1; i >= 0; --i) {
            if (slots[i][0] === slot) slots.splice(i, 1);
            break;
        }
    }

    function disconnectAll() {
        this.__slots = [];
    }

    function define(sender/* ... */) {
        Array.prototype.slice.call(arguments, 1).forEach(function (name) {
            var signal = sender[name] = function (/* ... */) {
                var slots = signal.__slots;
                var len = slots.length;
                if (len === 0) return;

                var args = [sender].concat(Array.prototype.slice.call(arguments));
                var newSlots = [];
                for (var i = 0; i < len; ++i) {
                    var slot = slots[i];
                    slot[0].apply(null, args);
                    if (!slot[1]) newSlots.push(slot);
                }
                signal.__slots = newSlots;
            }
            signal.__slots = [];
            signal.isSignal = true;
            signal.connect = connect;
            signal.disconnect = disconnect;
            signal.disconnectAll = disconnectAll;
        });
    }
})(window, document);

SignalSlotらしくconnect/disconnectというメソッド名にしておいたけど、attach/detachとかadd/removeの方がいいかなぁ。.NETっぽく似せるか、Qtっぽく似せるかによるけど。