イベントリスナーを登録するとき注目したい3つのオプション

UI開発者 宇賀

フロントエンドBlogにおいて最近の自分の記事ではWebアクセシビリティの話をずっとしてきたので、そろそろJavaScriptに関して個人的に気になっている内容について投稿します。

今回は「イベントリスナー」についてです。

少し前にVanilla.jsが話題となったことが記憶に新しいですが、なぜ今更イベントリスナー[1]の登録方法をテーマに持ち出すのかというと、個人的にaddEventListenerメソッドにおける第3引数の認知度が思いのほか低いと感じたからです。

addEventListenerを使おう

要素にイベントリスナーを登録する方法で最もメジャーなものは、addEventListenerを利用するケースではないでしょうか。

例えば、次のようなHTMLが用意されているとします。

<a href="https://www.mitsue.co.jp/" id="hoge">クリック</a>

次に、このようなJavaScriptコードを書きます。

document.getElementById('hoge').addEventListener('click', function (event) {
    event.preventDefault();

    alert('click.');
}, false);

リスナー関数内ではevent.preventDefault();を実行することで、要素が本来持つリンクの遷移機能を動作させないようにしています[2]

このような実装をした例を用意したので実際に試して見ましょう。JavaScriptが有効な環境であれば「クリック」というリンクをクリックした際に「click」というアラートダイアログが表示されます。

a要素にclickイベントを付与した例
クリック

例えば、先ほどのアラートダイアログを1度だけしか表示させたくなかったとすると、次のようなコードで実現できます。

let flag = false;

document.getElementById('hoge').addEventListener('click', function (event) {
    if (!flag) {
        event.preventDefault();

        alert('click.');
        flag = true;
    }
}, false);

あるいは次のように。

let targetElement = document.getElementById('hoge');
let handler = function (event) {
    event.preventDefault();

    alert('click.');
    targetElement.removeEventListener(event.type, handler);
};

targetElement.addEventListener('click', handler, false);

どちらも良くあるパターンではありますが、addEventListenerの第3引数に渡すオプションのonceプロパティをtrueに指定することで簡潔に実装することができます。

第3引数を使ってみよう

先ほどの機能をonceプロパティを用いて実装した例がこちらです。

let targetElement = document.getElementById('hoge');
let handler = function (event) {
    event.preventDefault();

    alert('click.');
};

targetElement.addEventListener('click', handler, {
    once: true
});

コード量だけを考えた場合、余計なフラグ変数やremoveEventListener()を実行せずに済むため、簡潔に書くことができます。

addEventListenerの第3引数に渡せるoptionsで利用されるプロパティは主に次の3つです。

  • capture
  • once
  • passive

第3引数がobject型ではなかった頃はuseCaptureをBoolean値で指定していましたが、現在はoptionsのcaptureプロパティがそれに相当します。

特にpassiveプロパティはイベント発火時に呼び出されるイベントハンドラがpreventDefault()を実行しないことを保証するかどうかを指定できる重要なプロパティです。

ブラウザはイベントが発火したとき、preventDefault()が実行されるかどうかを、イベントハンドラの処理が終わるまで待たなければなりません。

その影響でパフォーマンスに悪影響が出る場合があります。これは特に、touchmoveやscrollイベントに対してイベントリスナーを登録した場合顕著に現れます。

passiveプロパティがtrueの場合、イベントハンドラはpreventDefault()を呼び出すことができなくなります。

そのため、イベントハンドラの処理が終わるのを待たずとも元々の機能を制限しないことをブラウザに伝えることができます。

スクロールを止めたり、a要素で画面遷移をさせないなど、要素が本来持つ元々の機能を停止させないのであれば、preventDefault()が呼び出されないことをpassiveプロパティで保障してあげるべきでしょう。

複数の要素に対して同じイベントハンドラを渡す場合の一例を記載します。

<a href="https://www.mitsue.co.jp/">クリック</a>

<a href="https://www.mitsue.co.jp/" class="js-target">クリック</a>

<a href="https://www.mitsue.co.jp/" class="js-target">クリック</a>
let target = document.querySelectorAll('a.js-target');
let handler = function (event) {
    event.preventDefault();

    alert('click.');
};

[].forEach.call(target, function (element) {
    element.addEventListener('click', handler, {
        capture: false,
        once: true,
        passive: false
    });
});

あるいは次にように書くこともできます。

let target = document.querySelectorAll('a.js-target');
let i = 0;
let length = target.length;
let handler = function (event) {
    event.preventDefault();

    alert('click.');
};

for (i; i < length; i++) {
    target[i].addEventListener('click', handler, {
        capture: false,
        once: true,
        passive: false
    });
}

レガシーブラウザのサポートが次々と終了され、JavaScriptライブラリjQueryも徐々にその役割を終えようとしている雰囲気を感じている昨今、改めて既存のメソッドに注目しなおしてみるのもいいかもしれないですね。

[1]: この記事は、要素に「イベントタイプ」と「実行される関数」を紐づける仕組み自体をイベントリスナーと呼び、イベントハンドラとは「実行される関数」を指すものであるという理解の下で書いています。

[2]: ここではa要素の持つリンクとしての機能を止めていますが、実際にはリンクはリンクとしてページ移動を伴うべきだと思います。画面遷移させない場合はbutton要素を採用するか、role="button"を付与すると良いでしょう。