JavaScript async/await

async/await を説明する前に、まず JavaScript における非同期プログラミングの概念を理解する必要があります。参考:JavaScript 非同期プログラミング。

JavaScript はシングルスレッド言語であり、一度に1つのタスクしか実行できません。長時間実行されるタスクがメインスレッドをブロックするのを防ぐため、JavaScript は非同期プログラミングモデルを使用します。

非同期 vs 同期

  • 同期(Synchronous)プログラミング:コードは順番に実行され、前の操作が完了してから次の操作が実行されます。
  • 非同期プログラミング(Asynchronous):一部の操作は「タスクキュー」に置かれ、メインスレッドは後続のコードを実行し続け、メインスレッドが空いたときにキューのタスクを処理します。

サンプル

// 同期の例
console.log('1');
console.log('2');
// 出力: 1, 2

// 非同期の例
console.log('1');
setTimeout(() => console.log('2'), 0);
console.log('3');
// 出力: 1, 3, 2

コールバック関数の問題

async/await が登場する前、JavaScript は主にコールバック関数で非同期操作を処理していましたが、これは「コールバック地獄」(Callback Hell)を引き起こします。

サンプル

getData(function(a) {
  getMoreData(a, function(b) {
    getMoreData(b, function(c) {
      getMoreData(c, function(d) {
        console.log(d);
      });
    });
  });
});

このネスト構造により、コードが読みにくく、保守しにくくなります。


Promise の導入

ES6 では、コールバック地獄の問題を解決するために Promise オブジェクトが導入されました。

サンプル

function getData() {
  return new Promise((resolve, reject) => {
    // 非同期操作
    setTimeout(() => resolve('数据'), 1000);
  });
}

getData()
  .then(data => {
    console.log(data);
    return getMoreData(data);
  })
  .then(moreData => {
    console.log(moreData);
  })
  .catch(error => {
    console.error(error);
  });

Promise はコールバックの問題を改善しましたが、then() チェーン呼び出しはまだ直感的ではありません。


async/await 構文

ES2017 では async/await が導入されました。これは Promise の上に構築されており、非同期コードを同期コードのように見せることができます。

async 関数

関数宣言の前に async キーワードを付けると、その関数が非同期であることを示します:

async function fetchData() {
  // 関数本体
}

async 関数は常に Promise を返します:

  • 戻り値が Promise でない場合、自動的に resolved Promise にラップされます。
  • 例外がスローされた場合、rejected Promise が返されます。

await 式

await は async 関数内でのみ使用できます:

async function fetchData() {
  const result = await somePromise;
  console.log(result);
}

await は async 関数の実行を一時停止し、Promise の完了を待ちます:

  • Promise が resolve された場合、resolve された値を返します。
  • Promise が reject された場合、エラーがスローされます(try/catch でキャッチできます)。

実際の応用例

基本的な使い方

サンプル

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function showMessage() {
  console.log('开始');
  await delay(1000);
  console.log('1秒后');
  await delay(1000);
  console.log('又1秒后');
}

showMessage();

エラー処理

サンプル

async function fetchUserData() {
  try {
    const response = await fetch('https://api.example.com/user');
    if (!response.ok) {
      throw new Error('网络响应不正常');
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('获取数据失败:', error);
  }
}

並列実行

複数の非同期操作を並列に実行する必要がある場合は、Promise.all を使用できます:

サンプル

async function fetchMultipleData() {
  const [userData, productData] = await Promise.all([
    fetch('/api/user'),
    fetch('/api/products')
  ]);
 
  const user = await userData.json();
  const products = await productData.json();
 
  return { user, products };
}

よくある問題とベストプラクティス

1. await を忘れない

サンプル

// 間違った例 - await を忘れた場合
async function example() {
  const data = fetch('/api'); // await が欠落している
  console.log(data); // Promise オブジェクトを出力
}

// 正しい例
async function example() {
  const data = await fetch('/api');
  console.log(data); // 実際のデータを出力
}

2. 不必要な async を避ける

サンプル

// 不必要 - 関数内に await がない場合
async function unnecessaryAsync() {
  return 42;
}

// より簡単な書き方
function simpleFunction() {
  return 42;
}

3. トップレベル await

モジュールのトップレベルで await を直接使用できます(ES2022 の機能):

サンプル

// モジュール内
const data = await fetch('/api');
console.log(data);

4. パフォーマンスの考慮点

  • 順次実行 vs 並列実行:Promise.all を適切に使用してパフォーマンスを向上させる
  • エラー処理:考えられるすべてのエラーが確実にキャッチされるようにする

async/await と従来の Promise の比較

特徴 async/await Promise then/catch
可読性 高い、同期コードに似ている 中、チェーン呼び出し
エラー処理 try/catch を使用 .catch() を使用
デバッグ より容易、明確なコールスタックがある より困難、コールスタックが不明瞭な場合がある
コード構造 よりフラット ネストまたはチェーン

まとめ

async/await は JavaScript の非同期プログラミングにおける大きな改善であり、以下の特徴があります:

  1. 非同期コードをより読みやすく、保守しやすくする
  2. Promise に基づいており、既存の Promise コードと互換性がある
  3. より直感的なエラー処理方法を提供する
  4. デバッグ体験を改善する

async/await は Promise を完全に置き換えるものではありませんが、ほとんどの場合、よりエレガントな解決策を提供します。async/await の動作原理とベストプラクティスを理解することで、JavaScript の非同期プログラミング能力が大幅に向上します。

その他の拡張