TES Blog

株式会社テクニカルエンジニアリングサポートに所属する社員が、自身が携わるテクノロジーやイベントに関する情報を発信しています。

[JavaScript]イベントにもasync/awaitを使おう

はじめに

ECMAScriptが2015年から毎年バージョンアップするようになり、JavaScriptの仕様は大きく変わりました。 特に非同期処理まわりは、Promise(ECMAScript2015~)、async/await(ECMAScript2017~)が導入されたことで、 コールバック地獄の回避や可読性の向上などを実現できるようになりました。

また、JavaScriptではクリックやマウスホバーなどのイベントを検知する機能があります。 通常、イベント登録時に、イベント発火時に実行される処理をコールバックとして渡します。 実は、この従来の書き方をasync/awaitに置き換えると、一見すると同期処理的に書けるようにできます。

本記事では、以下の例のように、async/awaitへ置き換える話を説明します。

// いままでの書き方
const target = document.querySelector("#button");
target.addEventListener("click", () => {
  alert("クリックされた!");
});
// async/awaitを使った書き方
const target = document.querySelector("#button");
// ↓async/awaitを使えるように定義した関数(詳細は後述)。
// クリックされるまで、次の処理へ進まず非同期的に待機する。
await awaitForClick(target);
alert("クリックされた!");

考え方

awaitは、Promiseがsuccessされるまで、後続の処理へ進みません。 よって、イベント発火時にPromiseをresolveさせれば良いことになります。 具体的には、こんな感じです。
※今回はクリックイベントに限定していますが、抽象化すれば他のイベントでも応用出来ると思います。

const awaitForClick = target => {
  return new Promise(resolve => { // 処理A
    const listener = resolve;     // 処理B
    target.addEventListener("click", listener, {once: true}); // 処理C
  });
};

処理A:awaitForClick関数は、awaitで待機させるようPromiseのインスタンスを返すようにします。
処理B:イベント発火時にPromiseをresolveするリスナー(コールバック)を定義します。
処理C:処理Bで定義したコールバックを使って、イベント登録します。

これによって、awaitForClick関数は、引数に渡した要素がクリックされるまで、 次の処理へ進まなくなります。

なお、{once: true}で登録すると、1度しか発火しなくなります。 今回、それをオプションで渡しているのは、resolve関数を2回以上呼んでも意味が無いためです。 (resolve関数は、2回目以降何も起こりません)

使用例

クイズの回答を入力する画面で使うケースを紹介します。

JSFiddleでも動かせます。https://jsfiddle.net/fu0c3rnu/22/

<div id="question"></div>
<input type="text" id="user-input">
<button id="answer-button">回答!</button>
(async () => {
  const awaitForClick = target => { // 処理A
    return new Promise(resolve => {
      const listener = resolve;
      target.addEventListener("click", listener, {once: true});
    });
  };

  // 処理B
  const quiz = [
    {question: "日本で一番北に位置する都道府県は?", answer: "北海道",},
    {question: "日本の通貨は「円」。では、中国は?", answer: "元",},
    {question: "与えても無駄という意味のことわざ「豚に○○」。○○とは?", answer: "真珠",},
  ];

  let points = 0;
  for (const {question, answer} of quiz) {
    $("#question").text(question);               // 処理C
    await awaitForClick($("#answer-button")[0]); // 処理D
    const input = $("#user-input").val();        // 処理E
    if (!input || input !== answer) {            // 処理F
      alert(`残念!答えは${answer}でした!`);
    } else {
      points++;
      alert("正解です!");
    }
  }
  alert(`あなたの点数は${quiz.length}点中、${points}点でした!`); // 処理G
})();

処理A:awaitForClick関数を定義します。
処理B:出題する問題を定義します。
処理C:問題文を画面上に出します。
処理D:引数に渡しているボタン要素がクリックされるまで待機します。
処理E:入力された文字列を取得します。
処理F:答え合わせをし、アラートを出します。合っていれば点数を加算します。
処理G:点数をアラートで表示させます。

見ての通り、上から順番に実行されており、同期的処理のコードのように見えます。 処理の流れが追いやすく直感的なので、可読性が上がることが期待できますね。

おわりに

async/awaitを使うことで、同期処理ぽく書けるやり方を説明しました。 また、使用例としてクイズの回答を入力する画面を提示しました。 今回詳しく見ていませんが、モーダルウィンドウでも、いい感じに使えます。
https://jsfiddle.net/njny5xxo/3/

なお、今回のやり方はasync/awaitを実装しているブラウザでしか使えません。 各ブラウザの対応状況は、こちらで確認できます。
https://caniuse.com/#feat=async-functions
例によって、IE11では使えませんので、Babelなどを使う必要があります。