同じ要素に同じ種類のイベントを複数設定したときの実行順序を制御したい

JavaScriptで要素にイベントを設置していると、同じ要素に対して同じ種類のイベントを複数設定したい時があります。
その際の実行順序のルールや制御方法についてまとめてみました。


## 同じイベントの実行順序はイベントの追加順序に依存する
例えば、ボタン要素に複数のクリックイベントを設定してみましょう。
```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Sample Title</title>
  </head>
  <body>
    <button id="button">ここをクリック</button>
    <div id="div" class="js-keyup js-click"></div>
  </body>
  <script type="text/javascript">
    document.addEventListener("DOMContentLoaded", function () {
      // ボタン要素を取得
      const button = document.getElementById("button");

      // js-click-1のclickイベントを設置
      const jsClick1Handler = function () {
        console.log("js-click-1 clicked");
      };

      // js-click-2のclickイベントを設置
      const jsClick2Handler = function () {
        console.log("js-click-2 clicked");
      };

      // イベントリスナーの登録
      button.addEventListener("click", jsClick1Handler);
      button.addEventListener("click", jsClick2Handler);
    });
  </script>
</html>

```

この時、実行結果は以下のようになります。
![スクリーンショット 2024-06-16 13.54.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576247/0061ccf6-0d9b-6618-5029-f554ab930e34.png)

イベントリスナーの登録順序を入れ替えると、実行順序も変わります。
```javascript
// イベントリスナーの登録
button.addEventListener('click', jsClick2Handler);
button.addEventListener('click', jsClick1Handler);
```

![スクリーンショット 2024-06-16 13.56.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576247/0787453b-36a0-0fed-c406-13211ca2a613.png)

つまり、イベントリスナーは登録順序に従って実行されることがわかります。

## イベントの追加順序に依存することの問題点
イベントの追加順序に処理の順序が依存するため、常に追加順序を意識してコードを書く必要があります。

しかし、実際にイベントの追加順序を追うことは思ったよりも大変です。
例えば、先ほどのJavaScriptコードの一部が別ファイルに切り出されていたとしましょう。

```javascript:index.js
document.addEventListener("DOMContentLoaded", function () {
  // ボタン要素を取得
  const button = document.getElementById("button");
  // js-click-2のclickイベントを設置
  const jsClick2Handler = function () {
    console.log("js-click-2 clicked");
  };
  button.addEventListener("click", jsClick2Handler);
});
```
```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Sample Title</title>
  </head>
  <body>
    <button id="button">ここをクリック</button>
    <div id="div" class="js-keyup js-click"></div>
  </body>
  <script src="./index.js"></script>
  <script type="text/javascript">
    document.addEventListener("DOMContentLoaded", function () {
      // ボタン要素を取得
      const button = document.getElementById("button");

      // js-click-1のclickイベントを設置
      const jsClick1Handler = function () {
        console.log("js-click-1 clicked");
      };

      // イベントリスナーの登録
      button.addEventListener("click", jsClick1Handler);
    });
  </script>
</html>
```

一部のJavaScriptが切り出されると、`index.html`を見ただけでは実行順序を追うことが難しくなります。

上記のコードでは、まず`script`タグで切り出されている`jsClick2Handler`のイベントを登録します。
```html
<script src="./index.js"></script>
```
その後、`jsClick1Handler`のイベントが登録されます。
```javascript
// ボタン要素を取得
const button = document.getElementById("button");

// js-click-1のclickイベントを設置
const jsClick1Handler = function () {
    console.log("js-click-1 clicked");
};

// イベントリスナーの登録
button.addEventListener("click", jsClick1Handler);
```
よって、ボタンをクリックすると以下の通り実行されます。
![スクリーンショット 2024-06-16 14.29.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576247/f3bffcb0-8096-81ba-0ece-3c4bf5e85f85.png)

`index.js`の読み込み位置を変更すると、実行順序も変わります。
```html
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function () {
  // ボタン要素を取得
  const button = document.getElementById("button");

  // js-click-1のclickイベントを設置
  const jsClick1Handler = function () {
    console.log("js-click-1 clicked");
  };

  // イベントリスナーの登録
  button.addEventListener("click", jsClick1Handler);
});
</script>
<script src="./index.js"></script> <!-- ここに変更 -->
```
![スクリーンショット 2024-06-16 14.28.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576247/15613fc7-1087-b03f-3fb6-cfe6b86987f5.png)

## 解決案：イベントの実行順序を制御する
イベントの登録順序に依存せず実行順序を制御する案として、片方のイベント内でもう片方のイベントを呼ぶ方法があります。

```javascript
document.addEventListener('DOMContentLoaded', function() {
    // ボタン要素を取得
    const button = document.getElementById('button');

    // js-click-1のclickイベントを設置
    const jsClick1Handler = function(event) {
        jsClick2Handler.call(this, event);
        console.log('js-click-1 clicked');
    };

    // js-click-2のclickイベントを設置
    const jsClick2Handler = function(event) {
        console.log('js-click-2 clicked');  
    };

    // イベントリスナーの登録
    button.addEventListener('click', jsClick1Handler);
});
```

上記コードでは、`jsClick1Handler`内の最初の処理で`jsClick2Handler`を呼び出しています。また、イベントリスナーに登録しているのは`jsClick1Handler`のみです。

このようにイベントリスナーを複数登録せずに、片方のイベント処理で別のイベントの実行を制御すると、実行順序をコントロールできます。

### JavaScriptを切り出した時の例
JavaScriptの一部が切り出されている場合でも、同様に実行順序をコントロールできます。
下の例では、切り出された`jsClick2Handler`をimportで読み込み使用しています。

```html
<script type="module">
    // jsClick2Handlerの読み込み
    import { jsClick2Handler } from './index.js';

    document.addEventListener('DOMContentLoaded', function() {
        // ボタン要素を取得
        const button = document.getElementById('button');

        // js-click-1のclickイベントを設置
        const jsClick1Handler = function(event) {
            jsClick2Handler.call(this, event);
            console.log('js-click-1 clicked');
        };

        // イベントリスナーの登録
        button.addEventListener('click', jsClick1Handler);
    });
</script>
```
```javascript:index.js
// ボタン要素を取得
const button = document.getElementById("button");
// js-click-2のclickイベントを設置
export const jsClick2Handler = function () {
  console.log("js-click-2 clicked");
};
```

## まとめ
同じ要素に同じ種類のイベントを複数設置する場合、実行順序はイベントリスナーへの登録順序に依存します。
この対策として、イベントリスナーへの登録は一つの関数内にまとめ、イベントの実行順序を制御する方法があります。