【JavaScript】マウスによるクリックと element.click() ではリスナーの実行のされ方が違った【非同期処理】

# 概要

テストなどにおいてJavaScriptから擬似的に要素をクリックするために、`click()` メソッドや `dispatchEvent()` メソッドを使用したことがあるかもしれない。これらは実は非同期処理に関して"本当のマウスクリック"とは異なる振る舞いをすることがある。これを理解するには task、microtask などの JavaScript の非同期処理の実行モデルについて知る必要がある。

ちなみに、完全にユーザーの操作をシミュレートしたい場合は [puppeteer](https://github.com/GoogleChrome/puppeteer) のような直接ブラウザを操作するツールを使うといいだろう。

# よく知られている違い

本題の前に、本当のクリックと `click` メソッドの分かりやすい違いを2つあげる。

## 違い1: mousedown/mouseup イベントを伴うかどうか

マウスで要素をクリックすれば mousedown/mouseup イベントが発生するが、`click()` では発生しない。あくまで `click` イベントだけが発生するようになっている。

## 違い2: `event.isTrusted`

イベントオブジェクトの `isTrusted` プロパティはユーザーによる操作の場合 `true` 、JavaScriptからイベントを発生させる場合 `false` となる。

```js
el.addEventListener("click", e => {
  if (e.isTrusted) {
    console.log("本当にユーザーがクリックした")
  }
})
```

# 非同期処理に関する違い

まず、下の例で違いを確認しよう。1つ目のボタンには2つのクリックイベントリスナーが設定されており、それぞれログを出力する。2つ目のボタンをクリックすると1つ目のボタンに対して `click()` メソッドが実行される。

<p class="codepen" data-height="265" data-theme-id="light" data-default-tab="js,result" data-user="righteous_github" data-slug-hash="YmzLVb" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="click vs .click()">
  <span>See the Pen <a href="https://codepen.io/righteous_github/pen/YmzLVb/">
  click vs .click()</a> by righteous (<a href="https://codepen.io/righteous_github">@righteous_github</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

ブラウザによっては異なる結果になるかもしれないが、仕様に則った正しい動作としては以下のようになる。

```
// 直接クリック
0
1
3
2

// .click()
0
3
1
2
```

結論から言うと、この違いの原因は、

1. 複数のイベントリスナーが存在するとき、通常のクリックでは「1つのリスナーが実行し終わった後**コールスタックが一旦空になってから**次のリスナーが実行される」が、`click()` メソッドではコールスタックが空にならないまますべてのリスナーが実行されるから
2. microtask はコールスタックが空になるたびにすべて実行されるから

である。以降この2点についてそれぞれ説明する。

## コールスタック

コールスタックとは、「現在関数 f1 から呼び出された関数 f2 から呼び出された関数 f3 を実行中だ」という風に**現在どの関数を実行中で、どうやってここまできたか**という情報を格納するものである。コールスタックはその名の通り[スタック](https://ja.wikipedia.org/wiki/%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF)と呼ばれるデータ構造に格納されている。例えば下のように関数 `f` が内部で関数 `g` を呼び出すとしよう。

```js
function f() {
  const a = 1
  const b = a * 2
  g()
}

function g() {
  const c = 3
  console.log(c)
}

f()
```

関数が呼び出されるとそれがスタックに push され、関数の実行が終了すると pop される。最初に `f` が呼び出されると `f` が push される。`f` の中で `g` が呼び出されるとさらに `g` が push される。`g` の実行が終了するとスタックから pop され、やがて `f` も pop され、最後には空になる。

![call stack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/8392f27a-53a5-124b-4a3b-d957a0694923.png)

ブラウザの開発者ツールで実際のコールスタックの変化を確認することができる。たとえば下のスクリーンショットは Google Chrome で [www.google.com](https://www.google.co.jp/) を開いたときの状態を Performance タブで記録したものである。右側のつららのようなものがコールスタックの時間変化を表している(上の図とは上下逆になっていることに注意)。

![callstackgoogle.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/fad18a50-678a-1619-3dd4-f885b70bb65d.png)

## イベントリスナーとコールスタック

クリックイベントに対して2つのイベントリスナーが存在するとしよう。ユーザーがクリックした場合は、下の図の左のように、1つ目のリスナーが実行されたあと一旦コールスタックが空になり、そして2つ目のリスナーが実行される。一方、`click()` メソッドの場合は、**そのメソッドを呼び出した関数 (or 環境)**がまずコールスタックに存在し、その上で2つのリスナーが実行されるため、**途中でコールスタックが空にならない**。

![event listener & call stack.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/1153f08a-baad-e338-e29b-46e3ca1f44df.png)

## Microtask

JavaScript には、実行予定の非同期処理を蓄えておく **task queue** と **microtask queue** (キュー=待ち行列) というものがある。通常の非同期処理のコールバックは **task** と呼ばれ、task queue に蓄えられる。例えば `setTimeout(callback, time)` の `callback` である。**microtask** に該当するのは **Promise のコールバックと `MutationObserver` のコールバック**である。

microtask は task の軽量版である。microtask queue にある microtask は**コールスタックが空になると即実行される**。一方、task は前の task が完全に実行し終わらないと開始されない。

ここで最初の例に戻ろう。1つ目のイベントリスナーは `0` を出力したあと、microtask と task をそれぞれ queue に入れている。

```js
btn1.addEventListener("click", () => {
  log(0)
  Promise.resolve(1).then(log)  // <- microtask
  setTimeout(() => log(2), 0)   // <- task
})

btn1.addEventListener("click", () => {
  log(3)
})
```

通常のクリックでは、**1つ目のイベントリスナーが終了した直後にコールスタックが空になるため microtask が実行され** `1` が出力される。そして2つ目のイベントリスナーが実行され `3` が出力される。このタイミングでイベント処理のタスクが終了するため、次のタスクが実行され、`2` が出力される。

`click` メソッドでは2つ目のリスナーが終わるまでコールスタックが空にならないため、`0` `3` と出力されたあと microtask が実行され、`1` が出力される。

![event listener & call stack & microtask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/446383/f4e73027-7653-37df-ab1e-81f7b096fbca.png)


# 補足: `click()` と `dispatchEvent()` の違い

`click()` は `dispatchEvent(new MouseEvent("click", options))` とほぼ同じ振る舞いであると思われる[^spec]。ただし、 後者は `options` で「イベントがバブルアップするかどうか」やクリック座標など、イベントに関する細かい設定をすることができる。

[^spec]: 仕様の中で`click()` は[ここ](https://html.spec.whatwg.org/multipage/interaction.html#dom-click)、`dispatchEvent()` は [ここ](https://dom.spec.whatwg.org/#dom-eventtarget-dispatchevent)で定義されている。慎重に読んだわけではないが、大きな違いは見られなかった。

# 参考

- [Tasks, microtasks, queues and schedules - jakearchibald.com](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/)
- [Event.isTrusted - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted)
- [EventTarget.dispatchEvent() - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent)
- [MouseEvent - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent)