I.T.の
エンジニアブログ

Promiseとasync/awaitについて整理した【JavaScript】

公開: 2021/1/11更新: 2024/12/8

JavaScriptの非同期処理は難しい

JavaScriptの非同期な処理を書くのは頭がこんがらがりますよね。
初心者のころとかは特に、文が実行される順番とか、タイミングがどうなっているのかさっぱりわかりませんでした。
でもこれがまたよく使うので困ったものです。
最近でこそ慣れてきましたが、まだPromiseとかが入り乱れる処理を書くと訳が分からなくなるので、 この辺で一度整理しておこうと思いました。

今回は、あまり例がよろしくないかもしれませんが、クリックイベントを非同期処理に見立てていろいろ試していこうと思います。
ボタンがクリックされるまでの時間を非同期処理の返り値に見立てます。
10秒以上だったときはエラーにして、エラーハンドリングも試すことにします。

const async_task_callback = ($task_button, $container,
    previous_result, callback, error_callback) => {
  $task_button.appendTo($container);
  const today = new Date().getTime();
  $task_button.on('click', function() {
    const time = new Date().getTime() - today + previous_result;
    if (time > 10000) {
      error_callback({ time, $task_button });
      return;
    }
    callback(time);
  });
};

今回作ったソースはここに上げたのでよかったら見てください。

とりあえずcallbackから

何事も基本からということで、callbackでやってみます。

const task_button = '<p class="task">click</p>';
const $task_button = $(task_button);
async_task_callback($task_button, $ex1, 0, (result) => {
  print_result_on_success($task_button, result);
}, (error) => {
  print_result_on_error(error.$task_button, error.time);
});

非同期処理の結果を受け取った後にやりたいことを書いた関数をcallbackとして渡すだけ。わかりやすいですね。
しかし、逐次的に複数の非同期処理をやろうとすると大変なことになります。

const $task_button1 = $(task_button);
async_task_callback($task_button1, $ex2, 0, (result1) => {
  print_result_on_success($task_button1, result1);
  const $task_button2 = $(task_button);
  async_task_callback($task_button2, $ex2, result1, (result2) => {
    print_result_on_success($task_button2, result2);
    const $task_button3 = $(task_button);
    async_task_callback($task_button3, $ex2, result2, (result3) => {
      print_result_on_success($task_button3, result3);
    }, (error3) => {
      print_result_on_error(error3.$task_button, error3.time);
    });
  }, (error2) => {
    print_result_on_error(error2.$task_button, error2.time);
  });
}, (error1) => {
  print_result_on_error(error1.$task_button, error1.time);
});

いわゆるcallback hellというやつです。なので、できる限りPromiseを使おうということですね。

Promise

とりあえず、callbackを使う非同期処理をPromiseでラップします。 こんな風に、callbackで書かれている古いコードも、割と簡単にPromise化できます。
一からPromiseを作る場合は、new Promise()に渡す関数内に、何らかの時間のかかる処理を書いて、結果をresolveに渡せばよいです。

const async_task_promise =
  ($task_button, $container, previous_result) => new Promise(
    (resolve, reject) => {
      // new Promise()したタイミングですぐに実行される
      async_task_callback($task_button, $container,
        previous_result, resolve, reject);
    }
  );

new Promise()に渡す関数が実行されるタイミングが少しややこしいんですが、結論を言うとnewしたタイミングですぐに実行されます。

これを使えば、callback hellはPromise chainになります。

const $task_button1 = $(task_button);
const $task_button2 = $(task_button);
const $task_button3 = $(task_button);
async_task_promise($task_button1, $ex4, 0).then((result) => {
  print_result_on_success($task_button1, result);
  return async_task_promise($task_button2, $ex4, result);
}).then((result) => {
  print_result_on_success($task_button2, result);
  return async_task_promise($task_button3, $ex4, result);
}).then((result) => {
  print_result_on_success($task_button3, result);
}).catch((error) => {
  print_result_on_error(error.$task_button, error.time);
});

そして、Promiseでもう一つよく使うパターンが、複数の非同期処理をまとめて処理するものです。
具体的には、Promiseのリストを作って、Promise.all()などのメソッドを使うことで、 一つのPromiseを扱う時と同様に処理ができます。

const $task_buttons = [...new Array(3)].map(() =>
  $(task_button).on('click', function() {
    $(this).text('Clicked!').off();
  })
);
const promises = $task_buttons.map(($b) => async_task_promise($b, $ex5, 0));
Promise.all(promises).then((results) => {
  results.forEach((r, i) => {
    print_result_on_success($task_buttons[i], r);
  });
}).catch((error) => {
  print_result_on_error(error.$task_button, error.time);
});

async/await

Promiseはcallbackの上位互換的な存在でしたが、 じゃあasync/awaitはPromiseの上位互換なのかというと、そういうわけではないです。
最初そう勘違いしてて、Promiseをどうにかこうにかasync/awaitに書き直そうとかしてました。
主要なメリットは、Promiseの処理を見た目同期的に書くことができるということです。

上のPromise chainとほぼ同じ処理を書いてみました。
awaitキーワードの後にPromiseを置くことで、Promiseがresolveするまで待ち、その結果を返します。
ただし、awaitキーワードは、async functionの中に書かないといけません。
あとはその結果をよしなに処理するだけです。
例外処理はtry/catchがよく使われますが、 のちの処理で結果を使わないといけない場合letを使わないと書けないので、 例外処理だけPromise的にcatch()で書いてしまうのも手かもしれません。

// 以下のコードはasync functionの中に書く。

const $task_button1 = $(task_button);
const $task_button2 = $(task_button);
const $task_button3 = $(task_button);
// await/catch
const result1 =
  await async_task_promise($task_button1, $ex6, 0).catch((error) => {
    print_result_on_error(error.$task_button, error.time);
  });
if (result1) {
  print_result_on_success($task_button1, result1);
} else {
  return;
}
// try/catch
let result2; // need to use let (´·ω·`)
try {
  result2 = await async_task_promise($task_button2, $ex6, result1);
} catch (error) {
  print_result_on_error(error.$task_button, error.time);
}
if (result2) {
  print_result_on_success($task_button2, result2);
} else {
  return;
}
// then/catch
await async_task_promise($task_button3, $ex6, result2).then((result) => {
  print_result_on_success($task_button3, result);
}).catch((error) => {
  print_result_on_error(error.$task_button, error.time);
});

Promise.all()とかもPromiseを返すので、awaitの後にかけます。

// 以下のコードはasync functionの中に書く。

const $task_buttons = [...new Array(3)].map(() =>
  $(task_button).on('click', function() {
    $(this).text('Clicked!').off();
  })
);
const promises = $task_buttons.map(($b) => async_task_promise($b, $ex7, 0));
const results = await Promise.all(promises).catch((error) => {
  print_result_on_error(error.$task_button, error.time);
});
if (results) {
  results.forEach((r, i) => {
    print_result_on_success($task_buttons[i], r);
  });
}

あと、もう一つ言及するなら、async functionは返り値をそのまま返すわけではなく、 それをresolveするPromiseを返すということです。
関数内に待つ処理があるから、async function自体も非同期処理であるというわけですね。

まとめ

ようやくちゃんと整理できてよかったです。次からは自信をもって使えるといいなと思いました。