今回は、JavaScriptのaddEventListener/removeEventListenerを使う時に気をつける点を順を追って書いていく。特に無名関数をremoveEventListenerで消す方法を中心に書く。
addEventListenerは1つのターゲットにイベントリスナーを複数登録できる。例えば以下のような場合
window.addEventListener('click', function() { console.log(1); }, false); window.addEventListener('click', function() { console.log(2); }, false);
windowをクリックすればコンソールに1と2が表示される。この第二引数をイベントリスナーと言い、関数を指定する(以下、リスナーと書く)。今はaddEventListenerの中に無名関数をそのまま定義したが、関数は以下のように分けて書いてもいい。
function f() { console.log(1); } window.addEventListener('click', f, false);
しかし、この方法ではリスナーに引数を渡せない。リスナーに引数を渡したい場合は以下のいずれかの方法を使って書いたりする。
//1.処理したい関数を分けてリスナーの中で関数を呼び出す方法; function f(x) { console.log(x); } window.addEventListener('click', function(){ f(1); }, false); //2.bindを使う方法(上の方法とほとんど同じ); function f(x) { console.log(x); } window.addEventListener('click', f.bind(null, 1), false); //3.即時関数で処理したい関数を返す方法(たぶんメジャー); window.addEventListener('click', (function(x) { return function() { console.log(x) } })(1), false);
特にループ内で一気にイベントを定義するときに自分は3の方法をよく使う。ここで気をつけないといけないのはいずれもremoveEventListenerで削除できないという点である。また、通常であればaddEventListenerは全く同じ内容のイベントが重複すれば自動的に削除されるが、上のような方法で定義したリスナーはどこからも参照できないため自動的に消されることもない。これは、前のイベントを消して同じターゲットに違うイベントを再び定義したい場合や引数を変えて定義し直したい時に困る。強引に再び定義するとイベントがどんどん追加されてメモリリークを起こしてしまう可能性があるしイベント時の処理が増えるのでが無駄に重くなる。そこで、リスナーに引数を渡すこととremoveEventListenerで定義したイベントを消せることを両立する方法を書いていく。
イベントを消す1番目の方法は、リスナーの中でremoveEventListenerを使うという方法である。その際、リスナーには名前をつける。例えば上の3番目の方法をこう書き換えられる。
//3.即時関数で処理したい関数を返す方法の場合; window.addEventListener('click', (function(x) { return function f() { console.log(x); window.removeEventListener('click', f, false); } })(1), false);
ちなみにリスナーに名前を付けられない場合は、arguments.calleeを使う(arguments.calleeは非推奨)。この方法は少し使いづらい。この例だと一回イベントが発火すると同時に消える。条件分岐も使えるが、自分でイベントを消したいと思った時に必ずしも消せない。
そこで、2番目の方法。良い方法がStackOverflowにあったので少し書き換えて紹介。
var handler = (function(){ var events = {}, key = 0; return { addListener: function(target, type, listener, capture) { target.addEventListener(type, listener, capture); events[key] = { target: target, type: type, listener: listener, capture: capture }; return key++; }, removeListener: function(key) { if(key in events) { var e = events[key]; e.target.removeEventListener(e.type, e.listener, e.capture); } } }; }());
まず、addEventListenerを関数で囲む。ここでは、addListenerというメソッドを定義している。addListenerの引数にイベントターゲット、イベントタイプ、リスナー、キャプチャーを指定すればそれがそのままaddEventListenerでイベントとして定義される。更にaddListenerメソッドは渡した引数を全てをオブジェクトとして一つ上のスコープのeventsオブジェクトに格納する。そして今保存したイベントのkeyを返す。次のイベントのためにkeyはインクリメントする。こうすることによって定義したイベントを参照できる。そしてこのkeyをremoveListenerメソッドに渡すとそのkeyに格納されているイベントを削除できる。ここでは、handlerという変数にaddListenerメソッドとremoveListenerメソッドが入ったオブジェクトを返している。以下のように使う。
//イベントを定義すると同時に返ってきたkeyを取得; var key = handler.addListener(window, 'click', (function(x) { return function() { console.log(x); } })(1), false); //イベントを消す; handler.removeListener(key);
なんだかsetTimeoutのタイマーIDと似ている。このコードを見てわかるように引数も渡せて消すこともできる。ただ、自動的にイベントが消えるわけではないのでkeyを覚えておく必要がある。同じイベントを再び定義する時に自動的に消したいなら以下のようなコードを書ける。
var f = (function() { var key; return function() { //同じイベントが定義されていたら消す; handler.removeListener(key); //新しいkeyを取得; key = handler.addListener(window, 'click', (function(x) { return function() { console.log(x); } })(1), false); return key; } })();
f();
を何回実行しても自動的に前のイベントは消されるので重複することはない。少し読みにくいが、一応書いておく。なぜこんなことを書いているかというとループ内でaddEventListenerを使ってイベントを定義するときにどうするのがいいのか考えていたから。
上のようなコードをループ内でイベントを定義するときに適用すると以下のようなコードになる。
<p>あいうえお</p> <p>あいうえお</p> <p>あいうえお</p>
var f = (function() { var keys = []; return function() { var p = document.querySelectorAll('p'); for(var i = 0; i < p.length; i++) { handler.removeListener(keys[i]); keys[i] = handler.addListener(p[i], 'click', (function(i) { console.log(i); })(i), false); } return keys; } })();
ただ単にkeyを配列に変えただけ。ネストが深くなっているのがちと引っかかるが一例として。
最後にhandlerオブジェクトをIEのattachEventやdetachEventにも対応させたコードを書いて終わりとする。
var handler = (function(){ var events = {}, key = 0; return { addListener: function(target, type, listener, capture) { if(window.addEventListener) { target.addEventListener(type, listener, capture); }else if(window.attachEvent){ target.attachEvent('on' + type, listener); } events[key] = { target: target, type: type, listener: listener, capture: capture }; return key++; }, removeListener: function(key) { if(key in events) { var e = events[key]; if(window.removeEventListener) { e.target.removeEventListener(e.type, e.listener, e.capture); }else if(window.detachEvent){ e.target.detachEvent('on' + e.type, e.listener); } } } }; })();