LoginSignup
103
75

More than 3 years have passed since last update.

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

Posted at

概要

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

ちなみに、完全にユーザーの操作をシミュレートしたい場合は puppeteer のような直接ブラウザを操作するツールを使うといいだろう。

よく知られている違い

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

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

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

違い2: event.isTrusted

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

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

非同期処理に関する違い

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

See the Pen click vs .click() by righteous (@righteous_github) on CodePen.

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

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

// .click()
0
3
1
2

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

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

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

コールスタック

コールスタックとは、「現在関数 f1 から呼び出された関数 f2 から呼び出された関数 f3 を実行中だ」という風に現在どの関数を実行中で、どうやってここまできたかという情報を格納するものである。コールスタックはその名の通りスタックと呼ばれるデータ構造に格納されている。例えば下のように関数 f が内部で関数 g を呼び出すとしよう。

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

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

callstackgoogle.png

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

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

event listener & call stack.png

Microtask

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

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

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

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

補足: click()dispatchEvent() の違い

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

参考


  1. 仕様の中でclick()ここdispatchEvent()ここで定義されている。慎重に読んだわけではないが、大きな違いは見られなかった。 

103
75
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
103
75