第4版が利用可能です。こちらをご覧ください

第15章イベントの処理

あなたは自分の心をコントロールする力を持っています—外的な出来事ではなく。このことを理解すれば、あなたは強さを見つけるでしょう。

マルクス・アウレリウス, 瞑想録
Picture a Rube Goldberg machine

一部のプログラムは、マウスやキーボード操作などの直接的なユーザー入力を扱います。そのような入力は、きちんと整理されたデータ構造としては利用できません—リアルタイムで、断片的にやってきて、プログラムはそれが発生したときに反応することが期待されます。

イベントハンドラー

キーボードのキーが押されているかどうかを知る唯一の方法が、そのキーの現在の状態を読み取ることであるインターフェースを想像してみてください。キープレスに反応できるようにするには、キーが再び離される前にキャッチできるように、キーの状態を常に読み取る必要があります。キープレスを見逃す可能性があるため、時間のかかる他の計算を実行するのは危険です。

一部の原始的なマシンは、そのように入力を処理します。これよりも進歩した方法としては、ハードウェアまたはオペレーティングシステムがキープレスを検出し、それをキューに入れることです。プログラムは、新しいイベントがないかキューを定期的にチェックし、そこで見つけたものに反応できます。

もちろん、キューを見ることを覚えておく必要があり、また、キーが押されてからプログラムがイベントに気付くまでの時間がソフトウェアの応答性を低下させるため、頻繁にそれを行う必要があります。このアプローチはポーリングと呼ばれます。ほとんどのプログラマーはこれを避けることを好みます。

より良いメカニズムは、イベントが発生したときにシステムが積極的にコードに通知することです。ブラウザは、特定のイベントのハンドラーとして関数を登録できるようにすることで、これを行います。

<p>Click this document to activate the handler.</p>
<script>
  window.addEventListener("click", () => {
    console.log("You knocked?");
  });
</script>

windowバインディングは、ブラウザによって提供される組み込みオブジェクトを参照します。これは、ドキュメントを含むブラウザウィンドウを表します。そのaddEventListenerメソッドを呼び出すと、最初の引数で記述されたイベントが発生するたびに、2番目の引数が呼び出されるように登録されます。

イベントとDOMノード

各ブラウザのイベントハンドラーはコンテキストに登録されます。前の例では、windowオブジェクトでaddEventListenerを呼び出して、ウィンドウ全体に対するハンドラーを登録しました。このようなメソッドは、DOM要素やその他のオブジェクトタイプでも見つけることができます。イベントリスナーは、登録されたオブジェクトのコンテキストでイベントが発生した場合にのみ呼び出されます。

<button>Click me</button>
<p>No handler here.</p>
<script>
  let button = document.querySelector("button");
  button.addEventListener("click", () => {
    console.log("Button clicked.");
  });
</script>

この例では、ハンドラーをボタンノードにアタッチしています。ボタンのクリックによってそのハンドラーが実行されますが、ドキュメントの残りの部分のクリックでは実行されません。

ノードにonclick属性を与えることにも同様の効果があります。これはほとんどの種類のイベントで機能します—onを先頭に付けたイベント名である属性を通じてハンドラーをアタッチできます。

しかし、ノードは1つのonclick属性しか持つことができないため、その方法ではノードごとに1つのハンドラーしか登録できません。addEventListenerメソッドを使用すると、任意の数のハンドラーを追加できるため、要素に既に別のハンドラーがある場合でも、ハンドラーを追加しても安全です。

removeEventListenerメソッドは、addEventListenerと同様の引数で呼び出され、ハンドラーを削除します。

<button>Act-once button</button>
<script>
  let button = document.querySelector("button");
  function once() {
    console.log("Done.");
    button.removeEventListener("click", once);
  }
  button.addEventListener("click", once);
</script>

removeEventListenerに渡される関数は、addEventListenerに渡されたのと同じ関数値である必要があります。したがって、ハンドラーの登録を解除するには、両方のメソッドに同じ関数値を渡せるように、関数に名前(例ではonce)を付ける必要があります。

イベントオブジェクト

これまで無視してきましたが、イベントハンドラー関数には引数が渡されます。それはイベントオブジェクトです。このオブジェクトは、イベントに関する追加情報を保持します。たとえば、どのマウスボタンが押されたかを知りたい場合は、イベントオブジェクトのbuttonプロパティを見ることができます。

<button>Click me any way you want</button>
<script>
  let button = document.querySelector("button");
  button.addEventListener("mousedown", event => {
    if (event.button == 0) {
      console.log("Left button");
    } else if (event.button == 1) {
      console.log("Middle button");
    } else if (event.button == 2) {
      console.log("Right button");
    }
  });
</script>

イベントオブジェクトに格納される情報は、イベントの種類によって異なります。さまざまな種類については、この章の後半で説明します。オブジェクトのtypeプロパティは、常にイベントを識別する文字列("click""mousedown"など)を保持します。

伝播

ほとんどのイベントタイプでは、子を持つノードに登録されたハンドラーは、子で発生するイベントも受信します。段落内のボタンがクリックされた場合、段落のイベントハンドラーもクリックイベントを確認します。

ただし、段落とボタンの両方にハンドラーがある場合、より具体的なハンドラー—ボタンにあるハンドラー—が最初に実行されます。イベントは、発生したノードからそのノードの親ノード、さらにドキュメントのルートへと外側に伝播すると言われています。最後に、特定のノードに登録されたすべてのハンドラーが処理を終えた後、ウィンドウ全体に登録されたハンドラーがイベントに応答する機会を得ます。

どの時点でも、イベントハンドラーはイベントオブジェクトのstopPropagationメソッドを呼び出して、さらに上のハンドラーがイベントを受信するのを防ぐことができます。これは、たとえば、別のクリック可能な要素内にボタンがあり、ボタンのクリックによって外側の要素のクリック動作をアクティブにしたくない場合に役立ちます。

次の例では、ボタンとその周囲の段落の両方に"mousedown"ハンドラーを登録します。右マウスボタンでクリックすると、ボタンのハンドラーがstopPropagationを呼び出し、段落のハンドラーが実行されるのを防ぎます。別のマウスボタンでボタンをクリックすると、両方のハンドラーが実行されます。

<p>A paragraph with a <button>button</button>.</p>
<script>
  let para = document.querySelector("p");
  let button = document.querySelector("button");
  para.addEventListener("mousedown", () => {
    console.log("Handler for paragraph.");
  });
  button.addEventListener("mousedown", event => {
    console.log("Handler for button.");
    if (event.button == 2) event.stopPropagation();
  });
</script>

ほとんどのイベントオブジェクトには、発生元のノードを参照するtargetプロパティがあります。このプロパティを使用して、処理したくないノードから伝播されたものを誤って処理していないことを確認できます。

targetプロパティを使用して、特定のタイプのイベントを広く捉えることもできます。たとえば、ボタンの長いリストを含むノードがある場合、外側のノードに単一のクリックハンドラーを登録し、targetプロパティを使用してボタンがクリックされたかどうかを判断する方が、すべてのボタンに個別のハンドラーを登録するよりも便利な場合があります。

<button>A</button>
<button>B</button>
<button>C</button>
<script>
  document.body.addEventListener("click", event => {
    if (event.target.nodeName == "BUTTON") {
      console.log("Clicked", event.target.textContent);
    }
  });
</script>

デフォルトのアクション

多くのイベントには、それに関連付けられたデフォルトのアクションがあります。リンクをクリックすると、リンクのターゲットに移動します。下矢印を押すと、ブラウザがページを下方向にスクロールします。右クリックすると、コンテキストメニューが表示されます。などなど。

ほとんどの種類のイベントでは、JavaScriptのイベントハンドラーは、デフォルトの動作が行われるに呼び出されます。ハンドラーがこの通常の動作を望まない場合(通常はイベントの処理を既に完了しているため)、イベントオブジェクトでpreventDefaultメソッドを呼び出すことができます。

これは、独自のキーボードショートカットまたはコンテキストメニューを実装するために使用できます。また、ユーザーが期待する動作を不快に妨害するためにも使用できます。たとえば、これが辿ることができないリンクです。

<a href="https://developer.mozilla.org/">MDN</a>
<script>
  let link = document.querySelector("a");
  link.addEventListener("click", event => {
    console.log("Nope.");
    event.preventDefault();
  });
</script>

本当にそうする理由がない限り、そのようなことはしないでください。期待される動作が壊れていると、ページを使用する人々にとって不快になります。

ブラウザによっては、まったく傍受できないイベントもあります。たとえば、Chromeでは、現在のタブを閉じるためのキーボードショートカット(control-Wまたはcommand-W)は、JavaScriptでは処理できません。

キーイベント

キーボードのキーが押されると、ブラウザは"keydown"イベントを発生させます。離されると、"keyup"イベントが発生します。

<p>This page turns violet when you hold the V key.</p>
<script>
  window.addEventListener("keydown", event => {
    if (event.key == "v") {
      document.body.style.background = "violet";
    }
  });
  window.addEventListener("keyup", event => {
    if (event.key == "v") {
      document.body.style.background = "";
    }
  });
</script>

名前にもかかわらず、"keydown"はキーが物理的に押し下げられたときだけでなく、キーが押されたままのときにも、キーが繰り返されるたびにイベントが再度発生します。場合によっては、これに注意する必要があります。たとえば、キーが押されたときにDOMにボタンを追加し、キーが離されたときに再び削除する場合、キーが長押しされたときに誤って何百ものボタンを追加してしまう可能性があります。

例では、イベントオブジェクトの key プロパティを参照して、どのキーに関するイベントかを確認しました。このプロパティは、ほとんどのキーの場合、そのキーを押すことで入力される内容に対応する文字列を保持します。 Enter のような特殊キーの場合は、そのキーの名前を表す文字列(この場合は "Enter")を保持します。キーを押しながら Shift キーを押すと、キーの名前も影響を受ける可能性があります。たとえば、"v""V" になり、Shift + 1 でキーボードで "!" が生成される場合、"1""!" になることがあります。

ShiftControlAltMeta(Mac では Command)などの修飾キーも、通常のキーと同様にキーイベントを生成します。ただし、キーの組み合わせを調べるときは、キーボードとマウスのイベントの shiftKeyctrlKeyaltKeymetaKey プロパティを参照することで、これらのキーが押されているかどうかを確認することもできます。

<p>Press Control-Space to continue.</p>
<script>
  window.addEventListener("keydown", event => {
    if (event.key == " " && event.ctrlKey) {
      console.log("Continuing!");
    }
  });
</script>

キーイベントが発生する DOM ノードは、キーが押されたときにフォーカスを持っている要素によって異なります。ほとんどのノードは、tabindex 属性を与えない限りフォーカスを持つことはできませんが、リンク、ボタン、フォームフィールドなどはフォーカスを持つことができます。フォームフィールドについては、18章 で取り上げます。特に何もフォーカスを持っていない場合は、document.body がキーイベントのターゲットノードとして機能します。

ユーザーがテキストを入力しているとき、キーイベントを使用して入力された内容を把握することは問題があります。一部のプラットフォーム、特に Android スマートフォンの仮想キーボードでは、キーイベントが発生しません。しかし、旧式のキーボードを使用している場合でも、一部のテキスト入力は、キーの押下と単純には対応していません。たとえば、キーボードに収まらない文字を扱う人が使用するインプットメソッドエディター(IME)ソフトウェアでは、複数のキーストロークを組み合わせて文字が作成されます。

何かを入力したときに通知を受け取るには、<input><textarea> タグなど、入力可能な要素が、ユーザーがコンテンツを変更するたびに "input" イベントを発生させます。入力された実際のコンテンツを取得するには、フォーカスのあるフィールドから直接読み取るのが最適です。18章でその方法を説明します。

ポインターイベント

現在、画面上のものを指し示す方法として、マウス(タッチパッドやトラックボールなどのマウスのように動作するデバイスを含む)とタッチスクリーンの 2 つが広く使用されています。これらは異なる種類のイベントを生成します。

マウスクリック

マウスボタンを押すと、複数のイベントが発生します。 "mousedown" イベントと "mouseup" イベントは、"keydown" イベントと "keyup" イベントに似ており、ボタンが押されたときと離されたときに発生します。これらは、イベントが発生したときにマウスカーソルの真下にある DOM ノードで発生します。

"mouseup" イベントの後、ボタンの押下と離しの両方を含んだ最も具体的なノードで "click" イベントが発生します。たとえば、ある段落でマウスボタンを押して、別の段落にポインターを移動してボタンを離した場合、"click" イベントは、それらの段落の両方を含む要素で発生します。

2 回のクリックが近い間隔で発生すると、2 回目のクリックイベントの後に "dblclick"(ダブルクリック)イベントも発生します。

マウスイベントが発生した場所に関する正確な情報を取得するには、イベントの clientX および clientY プロパティを参照できます。これらのプロパティには、ウィンドウの左上隅を基準としたイベントの座標(ピクセル単位)が含まれています。または、ドキュメント全体の左上隅を基準とした pageX および pageY プロパティを参照できます(ウィンドウがスクロールされている場合は異なる場合があります)。

以下は、基本的な描画プログラムを実装したものです。ドキュメントをクリックするたびに、マウスカーソルの下にドットが追加されます。より基本的な描画プログラムについては、19章を参照してください。

<style>
  body {
    height: 200px;
    background: beige;
  }
  .dot {
    height: 8px; width: 8px;
    border-radius: 4px; /* rounds corners */
    background: blue;
    position: absolute;
  }
</style>
<script>
  window.addEventListener("click", event => {
    let dot = document.createElement("div");
    dot.className = "dot";
    dot.style.left = (event.pageX - 4) + "px";
    dot.style.top = (event.pageY - 4) + "px";
    document.body.appendChild(dot);
  });
</script>

マウスの動き

マウスカーソルが移動するたびに、"mousemove" イベントが発生します。このイベントは、マウスの位置を追跡するために使用できます。これが役立つ一般的な状況は、マウスドラッグ機能を実装するときです。

例として、以下のプログラムではバーを表示し、このバー上で左右にドラッグすると、バーが狭くなったり広くなったりするようにイベントハンドラーを設定します。

<p>Drag the bar to change its width:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
  let lastX; // Tracks the last observed mouse X position
  let bar = document.querySelector("div");
  bar.addEventListener("mousedown", event => {
    if (event.button == 0) {
      lastX = event.clientX;
      window.addEventListener("mousemove", moved);
      event.preventDefault(); // Prevent selection
    }
  });

  function moved(event) {
    if (event.buttons == 0) {
      window.removeEventListener("mousemove", moved);
    } else {
      let dist = event.clientX - lastX;
      let newWidth = Math.max(10, bar.offsetWidth + dist);
      bar.style.width = newWidth + "px";
      lastX = event.clientX;
    }
  }
</script>

"mousemove" ハンドラーはウィンドウ全体に登録されていることに注意してください。マウスがサイズ変更中にバーの外に出た場合でも、ボタンが押されている限り、サイズを更新する必要があります。

マウスボタンが離されたら、バーのサイズ変更を停止する必要があります。そのためには、現在押されているボタンに関する情報を示す buttons プロパティ(複数形に注意)を使用できます。これがゼロの場合、ボタンは押されていません。ボタンが押されている場合、その値はそれらのボタンのコードの合計になります。左ボタンのコードは 1、右ボタンのコードは 2、中央のボタンのコードは 4 です。たとえば、左ボタンと右ボタンが押されている場合、buttons の値は 3 になります。

これらのコードの順序は、button で使用されている順序とは異なり、中央のボタンが右ボタンよりも前に来ていることに注意してください。前述のように、ブラウザーのプログラミングインターフェイスの一貫性は、あまり高くありません。

タッチイベント

私たちが使用しているグラフィカルブラウザーのスタイルは、タッチスクリーンがまれだった時代に、マウスインターフェイスを念頭に置いて設計されました。初期のタッチスクリーン搭載携帯電話で Web を「機能」させるために、これらのデバイスのブラウザーは、ある程度、タッチイベントがマウスイベントであるかのように装っていました。画面をタップすると、"mousedown""mouseup"、および "click" イベントが発生します。

しかし、このイリュージョンはあまり堅牢ではありません。タッチスクリーンはマウスとは異なる動作をします。複数のボタンがなく、指が画面上にないときは指を追跡できず("mousemove" をシミュレートするため)、複数の指を同時に画面上に置くことができます。

マウスイベントは、単純なケースでのみタッチ操作をカバーします。ボタンに "click" ハンドラーを追加すると、タッチユーザーは引き続きそれを使用できます。ただし、前の例のリサイズ可能なバーのようなものは、タッチスクリーンでは機能しません。

タッチ操作によって発生する特定のイベントタイプがあります。指が画面に触れ始めると、"touchstart" イベントが発生します。タッチ中に移動すると、"touchmove" イベントが発生します。最後に、画面へのタッチが停止すると、"touchend" イベントが表示されます。

多くのタッチスクリーンは複数の指を同時に検出できるため、これらのイベントには関連付けられた座標の単一のセットはありません。むしろ、イベントオブジェクトには touches プロパティがあり、このプロパティには点の配列のようなオブジェクトが保持されており、各点には独自の clientXclientYpageX、および pageY プロパティがあります。

次のような操作を行って、タッチしているすべての指の周りに赤い円を表示できます。

<style>
  dot { position: absolute; display: block;
        border: 2px solid red; border-radius: 50px;
        height: 100px; width: 100px; }
</style>
<p>Touch this page</p>
<script>
  function update(event) {
    for (let dot; dot = document.querySelector("dot");) {
      dot.remove();
    }
    for (let i = 0; i < event.touches.length; i++) {
      let {pageX, pageY} = event.touches[i];
      let dot = document.createElement("dot");
      dot.style.left = (pageX - 50) + "px";
      dot.style.top = (pageY - 50) + "px";
      document.body.appendChild(dot);
    }
  }
  window.addEventListener("touchstart", update);
  window.addEventListener("touchmove", update);
  window.addEventListener("touchend", update);
</script>

多くの場合、タッチイベントハンドラーで preventDefault を呼び出して、ブラウザーのデフォルトの動作(スワイプ時のページのスクロールなど)をオーバーライドし、マウスイベント(ハンドラーがある可能性がある)が発生するのを防ぎます。

スクロールイベント

要素がスクロールされるたびに、その要素で "scroll" イベントが発生します。これは、ユーザーが現在何を見ているかを知る(画面外のアニメーションを無効にする、または悪の本部にスパイレポートを送信するなど)や、進行状況の表示(目次の特定部分を強調表示したり、ページ番号を表示したりするなど)など、さまざまな用途があります。

次の例では、ドキュメントの上にプログレスバーを描画し、下にスクロールすると塗りつぶされるように更新します。

<style>
  #progress {
    border-bottom: 2px solid blue;
    width: 0;
    position: fixed;
    top: 0; left: 0;
  }
</style>
<div id="progress"></div>
<script>
  // Create some content
  document.body.appendChild(document.createTextNode(
    "supercalifragilisticexpialidocious ".repeat(1000)));

  let bar = document.querySelector("#progress");
  window.addEventListener("scroll", () => {
    let max = document.body.scrollHeight - innerHeight;
    bar.style.width = `${(pageYOffset / max) * 100}%`;
  });
</script>

要素に fixedposition を設定すると、absolute ポジションとほぼ同じように動作しますが、ドキュメントの残りの部分と一緒にスクロールされなくなります。その効果は、プログレスバーを一番上に固定することです。現在の進捗状況を示すために、幅が変更されます。要素がページの幅を基準にサイズ変更されるように、幅を設定するときに単位として px ではなく % を使用します。

グローバルな innerHeight バインディングはウィンドウの高さを提供しますが、これはスクロール可能な合計の高さから差し引く必要があります。ドキュメントの最下部に到達した場合は、スクロールを続けることはできません。ウィンドウ幅の innerWidth もあります。現在のスクロール位置である pageYOffset をスクロール可能な最大位置で除算し、100 を掛けることで、プログレスバーのパーセンテージを取得します。

スクロールイベントで preventDefault を呼び出しても、スクロールは停止しません。実際、イベントハンドラーは、スクロールが実行されたにのみ呼び出されます。

フォーカスイベント

要素がフォーカスを取得すると、ブラウザーは "focus" イベントを発生させます。フォーカスを失うと、要素は "blur" イベントを受け取ります。

これらの 2 つのイベントと "scroll" のような一部のイベントは伝播しません。子要素がフォーカスを取得または失った場合、親要素のハンドラーには通知されません。

以下の例では、現在フォーカスがあるテキストフィールドのヘルプテキストを表示します。

<p>Name: <input type="text" data-help="Your full name"></p>
<p>Age: <input type="text" data-help="Your age in years"></p>
<p id="help"></p>

<script>
  let help = document.querySelector("#help");
  let fields = document.querySelectorAll("input");
  for (let field of Array.from(fields)) {
    field.addEventListener("focus", event => {
      let text = event.target.getAttribute("data-help");
      help.textContent = text;
    });
    field.addEventListener("blur", event => {
      help.textContent = "";
    });
  }
</script>

ユーザーがドキュメントが表示されているブラウザのタブやウィンドウ間を移動すると、ウィンドウオブジェクトは"focus"および"blur"イベントを受け取ります。

ロードイベント

ページがロードを完了すると、ウィンドウとドキュメントのbodyオブジェクトで"load"イベントが発生します。これは、ドキュメント全体が構築されたことを必要とする初期化アクションをスケジュールするためによく使用されます。<script>タグの内容は、タグが検出されたときにすぐに実行されることを覚えておいてください。たとえば、スクリプトが<script>タグの後ろに表示されるドキュメントの部分に対して何かを行う必要がある場合、これは早すぎる可能性があります。

外部ファイルをロードする画像やスクリプトタグなどの要素にも、参照するファイルがロードされたことを示す"load"イベントがあります。フォーカス関連のイベントと同様に、ロードイベントは伝播しません。

ページが閉じられるか、(例えば、リンクをたどって)移動されると、"beforeunload"イベントが発生します。このイベントの主な用途は、ユーザーがドキュメントを閉じることによって誤って作業を失うのを防ぐことです。このイベントのデフォルトの動作を防止、イベントオブジェクトのreturnValueプロパティを文字列に設定すると、ブラウザは、ユーザーに本当にページを離れたいかどうかを尋ねるダイアログを表示します。そのダイアログにはあなたの文字列が含まれる可能性がありますが、一部の悪意のあるサイトがこれらのダイアログを使用して、怪しげな減量広告を見るために人々をページに留まらせようとするため、ほとんどのブラウザはそれらを表示しなくなりました。

イベントとイベントループ

第11章で説明したイベントループのコンテキストでは、ブラウザのイベントハンドラーは他の非同期通知と同様に動作します。イベントが発生するとスケジュールされますが、実行されるためには、実行中の他のスクリプトが終了するのを待つ必要があります。

イベントが、他に何も実行されていない場合にのみ処理できるという事実は、イベントループが他の作業で拘束されている場合、(イベントを介して発生する)ページとのやり取りは、処理する時間ができるまで遅れることを意味します。したがって、長時間実行されるイベントハンドラーや多数の短時間実行されるイベントハンドラーで多くの作業をスケジュールすると、ページの動作が遅くなり、使いにくくなります。

ページをフリーズせずにバックグラウンドで時間のかかる処理を本当に行いたい場合は、ブラウザがWebワーカーと呼ばれるものを提供しています。ワーカーは、メインスクリプトと並行して、独自のタイムラインで実行されるJavaScriptプロセスです。

数値を二乗することが、別のスレッドで実行したい重く時間のかかる計算だと想像してください。メッセージに応答して二乗を計算し、メッセージを返信するcode/squareworker.jsというファイルを記述できます。

addEventListener("message", event => {
  postMessage(event.data * event.data);
});

複数のスレッドが同じデータに触れるという問題を回避するために、ワーカーはグローバルスコープやその他のデータをメインスクリプトの環境と共有しません。代わりに、メッセージをやり取りすることでワーカーと通信する必要があります。

このコードは、そのスクリプトを実行するワーカーを生成し、いくつかのメッセージを送信し、応答を出力します。

let squareWorker = new Worker("code/squareworker.js");
squareWorker.addEventListener("message", event => {
  console.log("The worker responded:", event.data);
});
squareWorker.postMessage(10);
squareWorker.postMessage(24);

postMessage関数はメッセージを送信し、受信側で"message"イベントを発生させます。ワーカーを作成したスクリプトは、Workerオブジェクトを介してメッセージを送受信しますが、ワーカーは、グローバルスコープで直接送受信することで、ワーカーを作成したスクリプトと通信します。JSONとして表現できる値のみがメッセージとして送信できます。相手側は値自体ではなく、それらのコピーを受け取ります。

タイマー

第11章setTimeout関数を見ました。これは、指定されたミリ秒後に、別の関数を後で呼び出すようにスケジュールします。

スケジュールした関数のキャンセルが必要になる場合があります。これは、setTimeoutによって返された値を格納し、それに対してclearTimeoutを呼び出すことによって行われます。

let bombTimer = setTimeout(() => {
  console.log("BOOM!");
}, 500);

if (Math.random() < 0.5) { // 50% chance
  console.log("Defused.");
  clearTimeout(bombTimer);
}

cancelAnimationFrame関数はclearTimeoutと同じように機能します。requestAnimationFrameによって返された値に対して呼び出すと、そのフレームはキャンセルされます (まだ呼び出されていない場合)。

同様の関数セットであるsetIntervalおよびclearIntervalは、Xミリ秒ごとに繰り返す必要があるタイマーを設定するために使用されます。

let ticks = 0;
let clock = setInterval(() => {
  console.log("tick", ticks++);
  if (ticks == 10) {
    clearInterval(clock);
    console.log("stop.");
  }
}, 200);

デバウンス

一部のタイプのイベントは、(たとえば、"mousemove"イベントや"scroll"イベントのように)連続して急速に多数発生する可能性があります。そのようなイベントを処理する場合は、時間のかかる処理をしないように注意する必要があります。そうしないと、ハンドラーが多くの時間を費やし、ドキュメントとのやり取りが遅く感じ始める可能性があります。

そのようなハンドラーで重要な処理を行う必要がある場合は、setTimeoutを使用して、頻繁に処理を行わないようにすることができます。これは通常、イベントのデバウンスと呼ばれます。これにはいくつかのわずかに異なるアプローチがあります。

最初の例では、ユーザーが何かを入力したときに反応したいのですが、すべての入力イベントに対してすぐに反応したくはありません。入力が速い場合は、一時停止が発生するまで待機したいだけです。イベントハンドラーですぐにアクションを実行する代わりに、タイムアウトを設定します。また、イベントが密接に発生した場合 (タイムアウト遅延よりも近い場合)、前のイベントからのタイムアウトがキャンセルされるように、前のタイムアウト (存在する場合) をクリアします。

<textarea>Type something here...</textarea>
<script>
  let textarea = document.querySelector("textarea");
  let timeout;
  textarea.addEventListener("input", () => {
    clearTimeout(timeout);
    timeout = setTimeout(() => console.log("Typed!"), 500);
  });
</script>

未定義の値をclearTimeoutに渡したり、既に発生したタイムアウトに対して呼び出したりしても、効果はありません。したがって、いつ呼び出すかを注意する必要はなく、すべてのイベントに対して実行します。

応答を特定の時間間隔で区切り、一連のイベントの後ではなく、イベント中にそれらを発生させたい場合は、わずかに異なるパターンを使用できます。たとえば、マウスの現在の座標を表示することによって"mousemove"イベントに応答したいが、250ミリ秒ごとにしか応答しないようにしたい場合があります。

<script>
  let scheduled = null;
  window.addEventListener("mousemove", event => {
    if (!scheduled) {
      setTimeout(() => {
        document.body.textContent =
          `Mouse at ${scheduled.pageX}, ${scheduled.pageY}`;
        scheduled = null;
      }, 250);
    }
    scheduled = event;
  });
</script>

まとめ

イベントハンドラーを使用すると、Webページで発生するイベントを検出して反応できます。addEventListenerメソッドは、そのようなハンドラーを登録するために使用されます。

各イベントには、それを識別するタイプ ("keydown""focus"など) があります。ほとんどのイベントは特定のDOM要素で呼び出され、その要素の祖先に伝播し、それらの要素に関連付けられたハンドラーがそれらを処理できるようにします。

イベントハンドラーが呼び出されると、イベントに関する追加情報を持つイベントオブジェクトが渡されます。このオブジェクトには、さらに伝播を停止するメソッド (stopPropagation) と、ブラウザによるイベントのデフォルトの処理を防止するメソッド (preventDefault) もあります。

キーを押すと、"keydown"および"keyup"イベントが発生します。マウスボタンを押すと、"mousedown""mouseup"、および"click"イベントが発生します。マウスを移動すると、"mousemove"イベントが発生します。タッチスクリーン操作では、"touchstart""touchmove"、および"touchend"イベントが発生します。

スクロールは"scroll"イベントで検出でき、フォーカス変更は"focus"イベントと"blur"イベントで検出できます。ドキュメントのロードが完了すると、ウィンドウで"load"イベントが発生します。

練習問題

風船

風船 (風船の絵文字、🎈を使用) を表示するページを作成します。上矢印キーを押すと、10パーセント膨らみ (拡大) し、下矢印キーを押すと、10パーセント縮みます (縮小)。

テキストのサイズ (絵文字はテキストです) は、親要素にfont-size CSSプロパティ (style.fontSize) を設定することで制御できます。値に単位 (たとえば、ピクセル (10px)) を含めることを忘れないでください。

矢印キーのキー名は"ArrowUp""ArrowDown"です。キーがページをスクロールせずに、風船だけを変更することを確認してください。

それが機能したら、風船を一定のサイズを超えて膨らませると爆発する機能を追加します。この場合、爆発とは、💥 絵文字に置き換えられ、イベントハンドラーが削除されることを意味します (したがって、爆発を膨らませたり縮めたりすることはできません)。

<p>🎈</p>

<script>
  // Your code here
</script>

"keydown"イベントのハンドラーを登録し、event.keyを見て、上矢印キーまたは下矢印キーが押されたかどうかを把握する必要があります。

現在のサイズは、新しいサイズのベースにできるようにバインディングに保持できます。イベントハンドラーから、また、おそらく開始時に初期サイズを設定するために、DOM内の風船のバインディングとスタイル両方を更新する関数を定義すると便利です。

テキストノードを別のノード (replaceChildを使用) に置き換えるか、親ノードのtextContentプロパティを新しい文字列に設定することで、風船を爆発に変更できます。

マウストレイル

JavaScriptの初期の頃は、アニメーション画像がたくさんある派手なホームページが全盛期であり、人々は言語を使用するための本当に刺激的な方法を思いつきました。

その一つがマウストレイルでした。これは、マウスカーソルをページ上で動かすと、その動きに追従する一連の要素のことです。

この演習では、マウストレイルを実装してもらいます。絶対位置指定された固定サイズと背景色を持つ<div>要素を使用してください(例については、「マウスクリック」セクションのコードを参照してください)。そのような要素をたくさん作成し、マウスが動いたときに、マウスカーソルの軌跡に沿って表示するようにしてください。

ここには様々なアプローチが考えられます。あなたのソリューションを、シンプルにも複雑にもできます。まず、簡単なソリューションとして、固定数のトレイル要素を保持し、それらを順に循環させて、"mousemove"イベントが発生するたびに次の要素をマウスの現在の位置に移動させることから始めるとよいでしょう。

<style>
  .trail { /* className for the trail elements */
    position: absolute;
    height: 6px; width: 6px;
    border-radius: 3px;
    background: teal;
  }
  body {
    height: 300px;
  }
</style>

<script>
  // Your code here.
</script>

要素の作成はループで行うのが最適です。それらを表示するためにドキュメントに追加してください。後で位置を変更するためにアクセスできるように、要素を配列に格納することをお勧めします。

それらの循環は、カウンタ変数を保持し、"mousemove"イベントが発生するたびに1を加算することで行うことができます。次に、剰余演算子(% elements.length)を使用して、特定のイベント中に配置したい要素を選択するための有効な配列インデックスを取得できます。

簡単な物理システムをモデル化することで、別の興味深い効果が得られます。"mousemove"イベントは、マウスの位置を追跡する一対のバインディングを更新するためだけに使用します。次に、requestAnimationFrameを使用して、トレイル要素がマウスカーソルの位置に引き寄せられるようにシミュレートします。アニメーションの各ステップで、ポインターに対する相対位置(および、オプションで、各要素に格納されている速度)に基づいて、それらの位置を更新します。これをうまく行う方法を考えるのはあなた次第です。

タブ

タブ付きパネルは、ユーザーインターフェースで広く使用されています。要素の上部に「突き出た」複数のタブから選択することで、インターフェースパネルを選択できます。

この演習では、単純なタブ付きインターフェースを実装する必要があります。DOMノードを受け取り、そのノードの子要素を表示するタブ付きインターフェースを作成する関数asTabsを作成します。ノードの上部に、各子要素に対して1つずつ、<button>要素のリストを挿入します。これらのボタンには、子要素のdata-tabname属性から取得したテキストを含めます。元の子供のうち1つを除くすべては非表示(displayスタイルをnoneに設定)にする必要があります。現在表示されているノードは、ボタンをクリックすることで選択できます。

それがうまくいったら、現在選択されているタブのボタンのスタイルを変更して、どのタブが選択されているかが明確になるように拡張してください。

<tab-panel>
  <div data-tabname="one">Tab one</div>
  <div data-tabname="two">Tab two</div>
  <div data-tabname="three">Tab three</div>
</tab-panel>
<script>
  function asTabs(node) {
    // Your code here.
  }
  asTabs(document.querySelector("tab-panel"));
</script>

陥る可能性のある落とし穴の1つは、ノードのchildNodesプロパティをタブノードのコレクションとして直接使用できないことです。まず、ボタンを追加すると、それらも子ノードになり、ライブデータ構造であるため、このオブジェクトに含められます。もう1つは、ノード間の空白のために作成されたテキストノードもchildNodesに含まれていますが、独自のタブを取得するべきではありません。テキストノードを無視するには、childNodesの代わりにchildrenを使用できます。

タブへの簡単なアクセスができるように、最初にタブの配列を構築することから始めることができます。ボタンのスタイルを実装するには、タブパネルとそのボタンの両方を含むオブジェクトを格納できます。

タブを切り替えるための別の関数を作成することをお勧めします。以前に選択したタブを保存し、それを非表示にして新しいタブを表示するために必要なスタイルのみを変更するか、新しいタブが選択されるたびにすべてのタブのスタイルを更新することができます。

インターフェースが最初のタブを表示した状態で開始するように、この関数をすぐに呼び出したい場合があります。