以前、「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 といった他のフレームワークにはイベント機能あるし、フレームワーク使わずに、イベント機能だけ使う、といったシーンも無いと思ったのでやめました。
2016/10/27 追記
Node の EventEmitter を使うようになったので、このライブラリは廃止。