Knockout.js にイベント機能を追加するプラグインを作ってみた

以前、「Knockout.js には Backbone.js みたいなルーティング機能が足りない」みたいなことを書きました。でも、Knockout.js はそもそもルーティング機能を本体で提供する気が無いようだし、Sammy.js や nav.js を併用すればいいかな、ってことで自己解決。

…そういえば足りない機能もう1つありました。イベント機能。ビューモデル間でメッセージをやり取りする手段が欲しい。例えば、子の削除要求を親に伝えたり、子が更新されたことを親に伝えたりするとき、今までコールバックや親の参照を渡したりしてました。何度「Closure Library や Backbone.js のようなイベント機能があれば」って思ったことか。

Backbone.js のイベント機能がシンプルで気に入ってたので、参考にしつつ、勉強も兼ねて実装してみました。ソースコードGitHub で公開しています。

本体を抜粋。

(function() {

// 名前空間を作成
if (!ko.events) {
    ko.events = {};
}

/**
 * Knockout.js にイベント機能を追加します。
 * @example
 *  var obj = {};
 *  ko.utils.extend(obj, ko.events.Event);
 */
ko.events.Event =  {
    /**
     * イベントハンドラを登録します。
     * @memberOf ko.events.Event#
     * @param {String} eventName イベント名
     * @param {Function} callback イベントハンドラ
     * @param {Object} context イベントハンドラを実行するときのコンテキスト
     * @example
     *  obj.bind("change", onChangeHandler);
     */
    bind: function(eventName, callback, context) {
        var calls = this._callbacks || (this._callbacks = {});
        var list = calls[eventName] || (calls[eventName] = []);
        // コールバックと、呼び出すときのコンテキストを追加
        list.push([callback, context]);
        return this;
    },

    /**
     * イベントハンドラを解除します。
     * @memberOf ko.events.Event#
     * @param {String} eventName イベント名
     * @param {Function} callback イベントハンドラ
     * @example
     *  obj.unbind();
     *  obj.unbind("change");
     *  obj.unbind("change", onChangeHandler);
     */
    unbind: function(eventName, callback) {
        if (!eventName) {
            // イベント名を省略したときは、
            // すべてのイベントハンドラを解除する
            this._callbacks = {};
            return this;
        }
        var calls = this._callbacks;
        if (!calls) {
            // イベントハンドラがまだ登録されていないとき
            return this;
        }
        if (!callback) {
            // 解除するイベントハンドラを省略したときは、
            // eventName のイベントハンドラをすべて解除する
            calls[eventName] = [];
            return this;
        }
        var list = calls[eventName];
        if (!list) {
            // イベントハンドラがまだ登録されていないとき
            return this;
        }
        for (var i = 0, len = list.length; i < len; i++) {
            if (list[i] && (callback === list[i][0])) {
                list[i] = null;
                break;
            }
        }
        return this;
    },

    /**
     * イベントを発生させます。
     * @memberOf ko.events.Event#
     * @param {String} eventName イベント名
     * @example
     *  obj.trigger("change");
     */
    trigger: function(eventName) {
        var calls = this._callbacks;
        if (!calls) {
            return this;
        }
        var list = calls[eventName];
        if (!list) {
            return this;
        }
        for (var i = 0, len = list.length; i < len; i++) {
            var callback = list[i];
            if(!!callback) {
                // コンテキストを取り出す
                var context = callback[1] || this;
                callback[0].apply(context, arguments);
            }
        }
        return this;
    }
};

})();

要らないと思った機能は省いています。すべてのイベントをハンドルする必要なんてないよね。

使い方はシンプル。イベント機能を追加したいオブジェクトに、ko.utils.extend でミックスインして使います。

// イベント機能追加
var vm = {};
ko.utils.extend(vm, ko.events.Event);

// イベントハンドラ
var onChanged = function() {
    alert("更新されました。");
};

// イベントハンドラ登録
vm.bind("changed", onChanged);

// イベント発生
vm.trigger("changed");

// イベントハンドラ解除
vm.unbind("changed", onChanged);

DOM のイベントにバインドすることはできません。DOM イベントにバインドするときはビューで data-bind 属性使えばいいでしょ。

Knockout.js の機能に依存しているわけじゃないので、プラグインじゃなくて汎用的な JavaScript ライブラリにした方がよかったかも。…と一瞬思ったけど、Backbone.js や Closure Library といった他のフレームワークにはイベント機能あるし、フレームワーク使わずに、イベント機能だけ使う、といったシーンも無いと思ったのでやめました。

2012/07/10 追記

Knockout.js 以外でも使うので、events.js に改名しました。

2016/10/27 追記

Node の EventEmitter を使うようになったので、このライブラリは廃止。