Event Listener: イベントリスナー

Event Listener とは

Event Listener とは、イベントを受け取って処理するものです。 JavaScript では関数がイベントリスナの役割を担うことが多いです。 ここでいうイベントとは、「画面上のどこかをクリックした」 だとか 「HTML の読み込みが完了した」 などといった事です。

従来は、イベントとイベントリスナ (関数) を結びつける方法としてイベントハンドラ式が使用されてきました。

// 「クリックされた」 というイベントを受け取るイベントリスナ (関数) myFunc を HTML 要素に結びつける例

// [(X)HTML] 要素の属性として記述する方法
<button onclick="myFunc();" id="test" />

// [JavaScript] element 要素のプロパティを使用する方法
//   id が "test" である要素を取得し、
//   取得した要素の onclick プロパティに関数 myFunc のポインタを代入
var element = document.getElementById("test");
element.onclick = myFunc;

// こうしておくと、当該要素がクリックされたときに関数 myFunc が実行される

JavaScript を使ったことがある人ならば上記のような記述を見たことがあると思います。 このような記法でイベントを処理する方式をイベントハンドラ式といいます。 昔から使われ、広く普及しているイベントハンドラ式ですが、近年は Ajax など JavaScript でも大規模なプログラミングをすることが多くなっており、イベントハンドラの欠点が問題となる場面が出てきました。 (具体的には、1 つの要素の onclick 属性に複数のプログラムがイベントリスナを代入しようとすると上書きされちゃって困るよね、といった問題。)

そこで、イベントハンドラ式ではない方法でイベントリスナを要素に結びつける方法をここでは紹介します。

Event Listener を追加 / 削除する関数

イベントハンドラ式を使用しない方法として、Event Listener を EventTarget (イベントの対象) に追加するメソッドが使用されます。 W3C によって標準化されたメソッドとして addEventListener があります。 また、IE には独自のメソッドとして attachEvent があります。

関数 addEventListener

関数 addEventListener は以下の形式で使用します。

obj.addEventListener( type, function, useCapture );

obj は、Event Listener を追加する対象で、XHTML で使う場合は主に element 型オブジェクトになります。 type は String 型で、イベントのタイプを表します。 例えば "click" や "load" などがあります。 function には関数を指定します。 この関数が Event Listener です。 実際にはオブジェクトを指定することもありますが、普通は関数を指定すると思います。 useCapture は boolean 型で、イベント伝播時のキャプチャリングフェーズで Event Listener が起動するかどうかを指定します。 true の場合はキャプチャリングフェーズで Event Listener が起動し、false の場合はターゲットフェーズ、及びバブリングフェーズで Event Listener が起動します。 IE にはこのようなフェーズの概念がない (?) みたいで、IE との互換性を考えると普通は false を指定します。

イベント伝播モデルのフェーズに関してはここでは割愛しますので、詳しく知りたい方は調べてみてください。 とりあえずは 「IE の attachEvent メソッドと同じ動きにするためには、useCapture は false にする」 と覚えておけばいいと思います。

また、同一の Event Listener (関数) 複数個を同じイベントターゲットに登録しようとしたとき、2 個目以降の Event Listener は破棄されます。

関数 attachEvent

IE には addEventListener メソッドがありません。 IE の場合は attachEvent メソッドを使用します。

obj.attachEvent( type, function );

objfunction については addEventListener 関数と同様です。
type については異なっており、addEventListener で指定するイベントタイプの名前の前に "on" をつける必要があります。 "onclick" や "onload" といった具合です。

同一の Event Listener (関数) 複数個を同じイベントターゲットに登録しようとしたとき、2 個目以降の Event Listener の扱いがどうなるのか、仕様はよくわかりませんが、IE8 で試してみたところ 2 個目以降も破棄されずに登録されました。 addEventListener メソッドと違う動作なので注意が必要です。

関数 removeEventListener

一度追加した Event Listener を削除したいときは関数 removeEventListener を使用します。

obj.removeEventListener( type, function, useCapture );

引数などは追加のときと同じです。

関数 detachEvent

当然ながら IE では removeEventListener を使えません。 IE では detachEvent 関数を使用します。

obj.detachEvent( type, function );

これも引数などは追加時と同様です。

Wrapping

Event Listener を登録するたびにいちいち addEventListener を使うか attachEvent を使うか判別していては面倒ですので、通常は wrapping して使用します。

[2008.12.07] IE 用のコードに問題があることに気づいたので修正しました。 IE 用のコードがやや煩雑になっており、無駄にメモリ空間も消費していますが、個人的な方針が IE に対して最適化することはしない、というものなのでこれでいいかなと思っています。 より良いコードがありましたら教えてください。

[2009.07.10] 更新
前回までは、イベントリスナ内から現在のイベントターゲットを取得するために this を使うようにしていました。 しかし DOM Events の勧告を見ると this を使うよりも引数の currentTarget プロパティを使うほうがいいみたいなので、IE でも Event オブジェクトで currentTarget プロパティが使えるように変更しました。 イベントが起こってイベントリスナが処理を行う前に 1 つ関数をはさみ、そこで引数の Event オブジェクトに currentTarget プロパティを設定する、という処理をしています。

Event Listener の追加 (クロスプラットフォーム)

/**
 * イベントリスナを追加する関数
 * 基本的には IE でも Firefox でも Safari でも Opera でも同じように使えるようにしてある. 
 * IE ではイベントリスナが受け取る引数のプロパティに currentTarget がないが, 
 * この関数を使うと自動的に追加してくれる. 
 * イベントリスナ内での this による参照は IE とその他で互換性を保っていないので, 
 * イベントリスナ内で現在のイベントターゲットを取得するときは, this を使うのではなく
 * 引数の currentTarget プロパティを使用すること. 
 *
 * 使い方:
 *     addListener( target, type, func );
 * パラメータ等:
 *     @param target イベントリスナを追加する対象. window オブジェクトや element オブジェクト.
 *     @param type イベントリスナが受け取るイベントのタイプ. "click" や "load" など. 
 *     @param func イベントリスナそのもの. 普通は関数. 
 *     @return 戻り値は boolean. addEventListener か attachEvent が実装されていれば true が返り, 
 *             それ以外の場合は false.
 */
var addListener = (function() {
    if ( window.addEventListener ) {
        // DOM Events 実装ブラウザ用
        return function(target, type, func) {
            target.addEventListener(type, func, false);
            return true;
        };
    } else if( window.attachEvent ) {
        // IE 用
        return function(target, type, func) {
            // ----- 局所変数の宣言 -----
            var i = 0;
            var hasBeenAdded = false;
            // ----- 処理 -----
            // target のプロパティに管理用配列を追加
            if( ! target._vividcode_el ) {
                target._vividcode_el = new Array(0);
                // unload 時に解体
                window.attachEvent("onunload", function myself(evt) {
                    // 配列の中身を null に
                    for( i = 0; i < target._vividcode_el.length; i++ ) {
                        target._vividcode_el[i][0] = null;
                        target._vividcode_el[i][1] = null;
                        target._vividcode_el[i] = null;
                    }
                    // 配列への参照をなくす
                    target._vividcode_el = null;
                    // 自分自身を detachEvent
                    window.detachEvent("onunload", myself);
                });
            }
            // 既に登録済みかどうかチェックする
            hasBeenAdded = false;
            for( i = 0; i < target._vividcode_el.length; i++ ) {
                if( target._vividcode_el[i][0] === func ) {
                    hasBeenAdded = true;
                    break;
                }
            }
            // 未登録の場合, 登録する
            if( ! hasBeenAdded ) {
                i = target._vividcode_el.length;
                target._vividcode_el[i] = new Array(func, function(evt) {
                        // evt.currentTarget を指定
                        evt.currentTarget = target;
                        // EventListener 起動
                        func(evt);
                    });
            }
            // addEventListener の方では, 同じ関数を二重に登録しようとすると 2 個目は破棄される. 
            // attachEvent の方だと 2 個目は破棄されない. 同一の動作になるよう, まず detachEvent する. 
            target.detachEvent("on"+type, target._vividcode_el[i][1]);
            target.attachEvent("on"+type, target._vividcode_el[i][1]);
            // unload 時に detachEvent しなければメモリリークを起こすとどこかで読んだので念のため. 
            // 必要以上に detachEvent する場合もあるが実害はないと思う 
            // (さすがに動作時間はそんなに変わらないでしょう) ので気にしないことにする.
            window.attachEvent("onunload", (function () {
                // target._vividcode_el も onunload イベントで解体するので, 
                // 下手すると参照前に解体されている可能性もある. 
                // よって, あらかじめ局所変数に読み込んでおく.
                var func = target._vividcode_el[i][1];
                return function myself(evt) {
                    target.detachEvent("on"+type, func);
                    window.detachEvent("onunload", myself);
                };
            })() );
            return true;
        };
    } else {
        // addEventListener も attachEvent も持ってないブラウザ用
        return function(target, type, func) {
            return false;
        };
    }
})();

Event Listener の削除 (クロスプラットフォーム)

/**
 * イベントリスナを削除する関数
 *
 * 使い方:
 *     removeListener( target, type, func );
 * パラメータ等:
 *     @param target イベントリスナを取り除く対象. window オブジェクトや element オブジェクト.
 *     @param type イベントリスナが受け取るイベントのタイプ. "click" や "load" など. 
 *     @param func イベントリスナそのもの. 普通は関数. 
 *     @return 戻り値は boolean. removeEventListener か detachEvent が実装されていれば true が返り, 
 *             それ以外の場合は false.
 */
var removeListener = (function() {
    if( window.removeEventListener ) {
        // DOM Events 実装ブラウザ用
        return function(target, type, func) {
            target.removeEventListener(type, func, false);
            return true;
        };
    } else if( window.detachEvent ) {
        // IE 用
        return function(target, type, func) {
            // ----- 局所変数の宣言 -----
            var i = 0;
            var hasBeenAdded = false;
            // ----- 処理 -----
            // func が登録されているかどうかチェックする
            hasBeenAdded = false;
            for( i = 0; i < target._vividcode_el.length; i++ ) {
                if( target._vividcode_el[i][0] === func ) {
                    hasBeenAdded = true;
                    break;
                }
            }
            // 登録されている場合, detachEvent
            if( hasBeenAdded ) {
                target.detachEvent("on"+type, target._vividcode_el[i][1]);
            }
            return true;
        };
    } else {
        // addEventListener も attachEvent も持ってないブラウザ用
        return function(target, type, func) {
            return false;
        }
    }
})();

Sample

wrapping した関数を用いて、ページのロードが終了した時点でダイアログボックスが表示されるという Sample を作成しました。

Sample ページは コチラ です。

次頁から、DOM を使ってページ内に新しく要素を追加したりするなどの処理を行っていくわけですが、ページのロードが終わる前に DOM をいじくると不具合が起こる可能性があります。 よって原則として、この Sample で使ったように、window オブジェクトで load イベントが発生した時点で実行される関数を作り、その関数内で DOM をいじっていくことになります。

Sample に使用した Code

// イベント・リスナを追加する関数
    var addListener = (function() {
        if ( window.addEventListener ) { // W3C 準拠ブラウザ用
            return function(elem, type, func) {
                elem.addEventListener(type, func, false);
            };
        } else if ( window.attachEvent ) { // IE 用
            return function(elem, type, func) {
                if( ! func._bridge ) {
                    func._bridge = new Array(0);
                }
                var i = func._bridge.length;
                func._bridge[i] = new Array(3);
                func._bridge[i][0] = elem;
                func._bridge[i][1] = type;
                func._bridge[i][2] = function() {
                    func.apply(elem, arguments);
                };
                elem.attachEvent("on"+type, func._bridge[i][2]);
            };
        } else {
            return function(elem, type, func) {
                return false;
            }
        }
    })();

    // イベント・リスナを削除する関数
    var removeListener = (function() {
        if ( window.removeEventListener ) {
            return function(elem, type, func) {
                elem.removeEventListener(type, func, false);
            };
        } else if ( window.detachEvent ) {
            return function(elem, type, func) {
                var i = 0; var f = null;
                while( i < func._bridge.length ) {
                    if( func._bridge[i][0] == elem && func._bridge[i][1] == type ) {
                        f = func._bridge[i][2];
                        break;
                    }
                    i++;
                }
                elem.detachEvent("on"+type, f);
                func._bridge.splice(i,1);
            };
        } else {
            return function(elem, type, func) {
                return false;
            }
        }
    })();

    // HTMLがブラウザにロードされたときに実行する処理
    var init = function(e) {
        alert("イベント " + e.type + " が発生しました");
        
        /* DOM をいじる時は、ここでいじるようにする */
        
    };

    // window オブジェクトに EventListener として init 関数を追加
    addListener(window, "load", init);