web-dev-qa-db-ja.com

関数をより複雑にすることでイベントリスナーを減らす必要がありますか?

たとえば、複数の入力があり、それぞれが完全に異なるプロセスに影響する場合(おそらく、1つはdivの幅を変更し、もう1つは予測検索のためにajaxリクエストを送信する)、単一のイベントをバインドしてswitchステートメントを使用して、関数を処理するか、各入力要素にバインドされた複数のeventListenersを設定しますか?

私が尋ねる理由の1つは、複数のイベントリスナーのパフォーマンスへの影響がよくわからないことですが、それらが無視できるとしても、読みやすさとコードメンテナンスの考慮事項が依然として回答に影響を与える可能性があります。

いくつかの考慮事項:

a)予想される使用頻度に依存しますか?

b)mousemoveなどの一般的なイベントは、1つのアプローチの方が優れていますが、inputは別のアプローチの方が優れていますか?

c)switchアプローチの方が良いと思われる場合、switchステートメントを拡張できる最大サイズ/複雑度。

8
user232573

探しているバズワードは、回答の検索に役立つ可能性がありますが、JavaScriptイベントの委任です。

イベントの委任には2つの基本的な種類があります。

  • コンポーネントの委任:ページ上のコンポーネントごとに、イベントごとに1つのリスナーが登録されます。

  • フロントコントローラーの委任:すべてのコンポーネントに対してイベントごとに1つのリスナーが登録され、HTML内の属性は、それぞれを表す関数またはオブジェクトへの動的ディスパッチに使用されます成分。

あなたが疑問に思っているのは、フロントコントローラーの種類です。

フロントコントローラーイベントの委任

これの実用的な例は、私が作成した Oxydizr というライブラリです。カスタムHTML属性data-actionsを使用して、ハンドラーを適切な「コントローラー」オブジェクトにディスパッチします。 switchステートメントではありませんが、提案するのと同じ基本ロジックに要約されます。

この抽象化レイヤーは、各ページで使用されるコンポーネントの数が不明で、UIをイベントリスナーに結び付ける追加のJavaScriptコードを記述したくない場合に役立ちます。

複雑さの増加は、拡張性と実装の容易さによってバランスが取れています。 HTMLコードと、そのHTML構造に固有の一連のJavaScriptコードを記述する必要はもうありません。適切な関数を呼び出す共通のルート要素ハンドルに、HTML属性のスプラッシュとイベントの委任を追加します。

a)予想される使用頻度に依存しますか?

はい。処理するイベントが多数ある場合、または相互作用を処理する必要のある要素が多数ある場合は、フロントコントローラーのイベントの委任が適しています。ここでの追加の利点は、正しく実行されれば、新しいコンポーネントの追加が非常に簡単になることです。

b)mousemoveなどの一般的なイベントは、1つのアプローチの方が優れていますが、別のアプローチの方が入力は優れていますか?

実際、mousemoveイベントは1秒あたり50回以上発生する可能性があるためです。ユーザーのスムーズなエクスペリエンスを維持するには、フロントコントローラーとハンドラーが20ミリ秒よりも速く実行する必要があります。インタプリタ言語としてはそれほど時間はかかりません!そのため、mousemovescrollのような「スパム行為」のあるイベントは、イベントの委任以外では、単独で処理する方が適切です。これは、このパターンのパフォーマンスに影響を与えます。

イベント委任のパフォーマンス

実行時のパフォーマンスは低下しますが、ページの読み込み時にパフォーマンスが向上します。ドキュメントツリーのノードが多いほど、ページ読み込みのパフォーマンスが向上することは明らかです。

JQueryのようなライブラリ、およびquerySelectorAllのようなネイティブDOM APIでは、CSSセレクターで要素をターゲットにできるため、隠されたダークサイドがあります。ブラウザーが特定のセレクター用に最適化しない限り、ブラウザーは基本的にCSSセレクターで要素を取得するときのドキュメントツリー内のすべての要素。これは、大きなページのページ読み込み時に特に顕著であり、より遅いプロセッサまたはより少ないRAMを搭載したモバイルデバイスでさらにmore顕著になります。したがって、次のようなコード:

var elements = document.querySelectorAll(".foo");

for (var i = 0; i < elements.length; i++) {
    elements[i].addEventListener(...);
}

Webアプリケーションが成長するにつれて、実行時間のコストが増加します。

1つの要素にリスナーを追加する方が、ページの読み込み時にはるかに高速です。私のお気に入りはdocument.documentElementです。これは<html>要素を参照し、JavaScriptの実行開始から存在しているためです。 DOMContentLoadedイベントや「DOM ready」イベントを待つ必要はありません。

次の2つの理由により、ランタイムパフォーマンスが低下します。

  1. イベントは、ソース要素で処理されるのではなく、ソース要素からバブリングした後に処理されます。これは最小限であり、JavaScriptでの影響を正確に測定できないほど小さくはないというのが私の経験です。

  2. イベントデリゲートは、フロントコントローラーであっても、個々のコンポーネントであっても、イベントの呼び出しごとに、ソース要素からドキュメントツリーまでJavaScriptでループし、処理できるものかどうかを確認する必要があります。

これらの2つの項目は、ユーザーがページを操作している間、パフォーマンスを低下させます。繰り返しになりますが、これは小さな削減​​であるため、これら2つの理由でイベントの委任を回避するのは時期尚早の最適化です。

フロントコントローラーを使用した動的ディスパッチ

c)switchアプローチの方が良いと思われる場合、switchステートメントの拡張を許可する必要がある最大サイズ/複雑度。

あなたがこれに疑問を投げかけているという単なる事実は、switchステートメントがこれを回避するための間違った方法であることを私に告げています。 switchステートメントは使用しないでください。 HTMLにいくつかのカスタムdata-*属性で注釈を付けて、これを簡単に拡張し、コストをかけないようにする必要があります。

<button type="button"
        data-action="confirm"
        data-confirm="Are you sure?">
    Delete Item?
</button>

この素朴な実装では、「フロントコントローラ」にリスナーを登録します。

frontController.addListener("confirm", function(event) {
    if (!confirm(event.target.getAttribute("data-confirm")) {
        event.preventDefault();
    }
});

これを別のコンポーネントに拡張するのは簡単です。

frontController.addListener("removeItem", function(event) { ... });

この例の実装:

var frontController = {
    listeners: {},

    addListener: function(actionName, callback) {
        this.listeners[actionName] = callback;
    },

    handleClick: function(event) {
        var listeners = frontController.listeners,
            action = event.target.getAttribute("data-action");

        if (listeners[action]) {
            listeners[action](event);
        }
    }
};

document.documentElement.addEventListener("click", frontController.handleClick, false);

HTMLドキュメントに埋め込まれたデータを使用すると、1つの決定構造を記述してすべてのコンポーネントを処理できるため、switchステートメントは不要です。これは、イベント委任のためにフロントコントローラーパターンを実装する場合に必要な場所です。

したがって、関数をより複雑にすることでイベントリスナーを減らす必要がありますか?

はい、次の場合:

  • さらに多くのコンポーネントを追加する予定です
  • イベントリスナーを追加するコードを記述したくない
  • ドキュメントツリーから削除された要素のイベントリスナーを削除するコードを記述したくない(メモリリークが気にならない限り)

いいえ、次の場合:

  • 多くのJavaScript作業を行う必要はありません
  • 要素がページに動的に追加されたときにイベントリスナーを追加するコードを記述してもかまいません
  • まだアクティブなイベントリスナーが残っている削除された要素からのメモリリークは気にしません。

    そして、はい、メモリリークを気にしない場合があります。ページが非常に長い間使用されていない場合、メモリリークは大きな問題ではない可能性があります。シングルページアプリケーションの場合...[〜#〜]ロット[〜#〜]のRAMがあることを願っています。

10
Greg Burghardt

この特定のシナリオでスイッチを使用したことはありませんが、一見すると冗長なようです。

基本的に、イベントとハンドラー関数の間に、ハンドラー関数を呼び出す以外に何もしないレイヤーを追加しています。

これにより、たとえば、ハンドラー関数を呼び出す前にいくつかのコードを実行する必要がある場合に、スイッチMIGHTを使用する意味があるユースケースがわかります。これは、各ハンドラー関数からそのコードを呼び出すのに比べて、よりすっきりしたデザインになる可能性があります。

3
pvukovic

Open-Closed Principle に違反しているため、リスナー自体が拡張可能に設計されていない限り、単一のリスナーを使用しないでください。サブコンポーネントの内部の詳細を「親」にリークすることを提案する。

フィーチャーBとCで構成されるフィーチャーAがある非常に単純な例を取り上げます。

単一の拡張不可能なリスナーアプローチでは、このような設計を強いられる可能性があります。

function initFeatureA(domEl) {
    initFeatureB(domEl);
    initFeatureC(domEl);

    domEl.addEventListener('change', function (e) {
        if (...) doSomethingRelatedToFeatureA();
        else if (...) doSomethingRelatedToFeatureB();
        else if (...) doSomethingRelatedToFeatureC();

        //#1. OCP violation because as we add features we need to change the handler
        //#2. Not cohesive because we might have a very large spectrum of unrelated behaviors

        //#3. The design invites us to leak logic of sub-features into feature A
    });
}

function initFeatureB(domEl) {
    //...
}

function initFeatureC(domEl) {
    //...
}

以下と比較してください。すべての機能/コンポーネントがより適切にカプセル化されています。

function initFeatureA(domEl) {
    initFeatureB(domEl);
    initFeatureC(domEl);

    domEl.querySelector('.feature-a-input').addEventListener('change', function (e) {
        doSomethingRelatedToFeatureA();
    });
}

function initFeatureB(domEl) {
     domEl.querySelector('.feature-b-input').addEventListener('change', function (e) {
        doSomethingRelatedToFeatureB();
    });
}

function initFeatureC(domEl) {
     domEl.querySelector('.feature-c-input').addEventListener('change', function (e) {
        doSomethingRelatedToFeatureC();
    });
}

さて、単一のハンドラーが複数のハンドラーよりも理にかなっている場合がありますが、その単一のハンドラーは一般に、上記の懸念のいくつかを回避するために拡張可能に設計されています。たとえば、個々のキーがアクションにバインドされるゲームを想像してみてください。

押されたキーをキャプチャする低レベルの詳細を抽象化するInputControllerコンポーネントを設計する可能性があります。次に、他のコンポーネントはアクションを実行する必要があるため、InputControllerを介して自分自身を登録できます。

1
plalx