非同期処理を改めて理解する


こんにちは、上條(mk-0A0)です。
前回の記事で fetch をしっかり学んでみる という内容を投稿し、そのなかで非同期処理について少し触れました。今回は非同期処理について改めて調べてみます。

同期処理と非同期処理

まずは同期処理と非同期処理の理解からです。

同期処理とは、複数の処理を一つずつ実行していくことです。実行している処理が完了するまで次の処理には移りません。
非同期処理とは、処理を実行するメインスレッドから一時的に処理を切り離し、後続の処理と並行して処理することです。
メインスレッドの概要は以下をご覧ください。
Main thread (メインスレッド) – MDN Web Docs 用語集

非同期処理が行えるのは次に紹介する3つです。

非同期処理① setTimeout

setTimeout は本来非同期処理のための関数ではないですが、第2引数に指定した時間分 setTimeout 内の処理が遅れて実行されます。
以下のサンプルコードを実行すると setTimeout が先に実行されますが、「非同期処理 完了」より先に「同期処理 完了」がコンソールに表示されます。setTimeout は実行されると3秒間待機状態になり、その間に後続の処理である console.log が実行されるためこのような挙動になります。

setTimeout(() => {
  console.log("非同期処理 完了");
}, 3000);

console.log("同期処理 完了");

デメリットとしては秒数を指定するため実行のタイミングが正確性に欠けるのと、連続した処理を書くには入れ子にする必要があるため階層が深くなり複雑になる、いわゆるコールバック地獄になることが挙げられます。
以下は1秒おきにコンソールが表示されるサンプルです。これが複雑な処理となると見通しが悪くなってしまいます。

setTimeout(() => {
  console.log("非同期処理 完了1");
  setTimeout(() => {
    console.log("非同期処理 完了2");
    setTimeout(() => {
      console.log("非同期処理 完了3");
    }, 1000);
  }, 1000);
}, 1000);

このコールバック地獄を解決するのが次にご紹介する Promise です。Promise だと以下のように書き換えられます。連続した処理を .then で繋いで書けるため階層が深くなる問題が解決できます。

// 1秒後にconsoleを出力してresolveを返す関数
const sample = (message) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(message);
      resolve();
    }, 1000);
  });
};

sample("promise")
  .then(() => sample("非同期処理 完了1"))
  .then(() => sample("非同期処理 完了2"))
  .then(() => sample("非同期処理 完了3"));

非同期処理② Promise

Promise のコールバック関数の引数は resolve(成功)と reject(失敗)を取ります。コールバック関数内の処理が成功した場合は then へ、失敗した場合は catch へ処理が移行します。finaly は処理の結果がどちらであっても最終的に実行されます。

new Promise((resolve, reject) => {
  // 処理
  const error = new Error();
  if (error instanceof Error) {
    reject();
  } else {
    resolve();
  }
})
  .then(() => {
    console.log("resolve");
  })
  .catch(() => {
    console.error("error");
  })
  .finally(() => {
    console.log("finally");
  });

出力結果は以下のようになります。

const error が Error でなければ resolve が実行される
const error が Error の場合 reject が実行される

非同期処理③ async/await

async/await は Promise をより直感的にした記述です。async がついている関数は Promise を返し、await は非同期処理の完了を待ちます。then を記述する必要がなくなり、よりシンプルに記述できました。

// 1秒後にconsoleを出力してresolveを返す関数
const sample = (message) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(message);
      resolve();
    }, 1000);
  });
};

const asyncFunctionSample = async () => {
  console.log("promise");
  await sample("非同期処理 完了1");
  await sample("非同期処理 完了2");
  await sample("非同期処理 完了3");
};

asyncFunctionSample();

Promise と async/await の使い分け

Promise と async/await どちらを使えばいいか迷ってしまうと思います。使い分けとしては、複数(多数)の並列処理では Promise.all が使える Promise が有効です。Promise.all でも then が使えるため、複数の非同期処理の結果がすべて揃ってから後続の処理を実行できます。

// 1秒後にconsoleを出力してresolveを返す関数
const sample = (message) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(message);
      resolve();
    }, 1000);
  });
};

Promise.all([
  sample("非同期処理 完了1"),
  sample("非同期処理 完了2"),
  sample("非同期処理 完了3"),
]).then(() => {
  console.log("resolve");
});

参考

setTimeout() – Web API | MDN
Promise – JavaScript – MDN Web Docs
async function – JavaScript – MDN Web Docs – Mozilla
await – JavaScript – MDN Web Docs

まとめ

個人的に setTimeout からどういう経緯で今の非同期処理が成り立っているのかを確認できたのが一番大きなポイントでした。Promise や async/await をなんとなくで使っていたので、なぜ fetch で非同期処理が使われるの理解が深まったと思います。


投稿者 Kamijo Momoka

フロントエンドエンジニア。
HTML/CSS/JavaScript/WordPressでのサイト制作からNext.js/TypeScriptなどを使ったWebアプリ開発、FigmaでのUIデザインまで広く経験しています。 デザインエンジニアと名乗るのが夢です。