AbortControllerでEventListenerを取り外す方法

こんにちは！ちゅらデータのおーすてぃんやいびーん！

# 概要
AbortControllerでEventListenerを取り外す方法を紹介します。

## 課題
EventListenerを取り外すことはメモリリークを防ぐのにも、思わぬ動作を抑制するのにも、必要な作業です。

従来のやり方では、EventTarget.removeEventListenerのメソッドを使って取り外していました。ただ、このやり方には以下の問題があります。
1. EventTarget.addEventListenerを実行した時に指定したEventListenerのEventタイプと完全に一致するようにEventTarget.removeEventListenerに渡さないと静かに失敗する
1. EventTarget.addEventListenerを実行した時に指定した関数の<a href="https://developer.mozilla.org/ja/docs/Glossary/Object_reference">Object reference (オブジェクト参照)</a>を2番目の引数として渡さないと、静かに失敗する。こちらは、下記解説します。
1. 「静かに失敗する」という表現ですが、EventTarget.removeEventListenerが意図しているEventListenerを取り外せなかったとしても、エラーをthrowしないという意味です。つまり、うまく取り外せたかどうか、実験してみないとわからないということです。

2番目の問題点ですが、これはJavaScriptのメモリ管理を理解していないと陥る落とし穴です。

実は、<a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions#%E9%96%A2%E6%95%B0%E5%BC%8F_function_%E5%BC%8F">関数式の関数、無名関数</a>でEventTarget.addEventListenerを実行してしまった場合は、EventTarget.removeEventListenerで従来のやり方では取り外しようがないのです。

以下のコードを見て考えましょう。

```javascript
const button = document.querySelector("button");

button.addEventListener("click", () => doSomething());

button.removeEventListener("click", () => doSomething());
```

僕がまだJavaScriptを理解していなかった頃に、このようなコードを書いて大変苦労したことがありますが、これは静かに失敗するのです。なぜなら、2行目でaddEventListenerに渡している関数式の関数は、３行目の関数式の関数とは中身が一緒でも同一のオブジェクトではないからです。

ご存知の方も多いと思いますが、JavaScriptは原始値（文字列、数字、Symbol等）以外のオブジェクト（関数もオブジェクトです）をHeapというメモリの架空の場所に保管しています。関数を作る時に、関数のオブジェクトがHeapに保管され、Heapのどこに保管されているのかの住所を返すのです。

以下のコードを見て考えましょう。

```javascript
const button = document.querySelector("button");

const handleClick = () => doSomething()

button.addEventListener("click", handleClick);

button.removeEventListener("click", handleClick);
```

このコードなら、EventListenerを無事に取り外してくれます。なぜなら、３行目で追加したEventListenerの引数に渡した関数のHeap内の住所を教えているからです。

handleClickの定数には、関数が入っているのではなく、関数のHeap内の住所が保管されているのです。

やや脱線しましたが、上記のような書き方が許されない場面があり、困ります。

## 解決法
EventTarget.addEventListenerを実行する時に、3番目のOptionsの引数のObjectに、signalを渡します。

上記の例だと以下のようになります。

```javascript
const button = document.querySelector("button");

const controller = new AbortController();

button.addEventListener("click", () => doSomething(), { signal: controller.signal });

controller.abort();
```
これで無事にEventListenerを取り外せるのです！また同時に複数のEventListenerも外せるので、非常に簡単になります。

# 例

### コード
```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <button id="abort">Abort Event Listener</button>
  <button id="test">Test Event Listener</button>
</body>

<script>
  const abortEventListenerButton = document.querySelector("button#abort");
  const testEventListenerButton = document.querySelector("button#test");

  const controller = new AbortController();

  testEventListenerButton.addEventListener(
    "click",
    () => {
      const currentColor = testEventListenerButton.style.backgroundColor;
      if (!currentColor || currentColor === "blue") {
        testEventListenerButton.style.backgroundColor = "red";
      } else {
        testEventListenerButton.style.backgroundColor = "blue";
      }
    },
    { signal: controller.signal }
  );

  abortEventListenerButton.addEventListener("click", () => controller.abort());
</script>
</html>
```

### 結果
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2344956/8197c060-1f4b-a060-d068-2617ba467623.gif)

# まとめ
いかがでしょうか？筆者はこのAbortControllerの意外な使い方に驚き、ウキウキした気持ちで共有したくなりました。
JavaScriptは奥が深く、知れば知るほど楽になりますね。