Traffine I/O

日本語

2022-02-21

非同期JavaScript

非同期JavaScript

JavaScriptはシングルスレッドの言語であり、一度に1つのタスクしか処理できません。しかし、サーバーからのデータの取得、ファイルの読み込み、データベースのクエリなど、特定の操作には時間がかかることがあります。ここで非同期JavaScriptが登場します。非同期JavaScriptを使用すると、これらの操作の完了を待つ間にJavaScriptエンジンが他のタスクを実行できます。

同期プログラミングでは、各タスクが順番に実行されます。つまり、次のタスクが開始される前に前のタスクが完了する必要があります。一方、非同期プログラミングでは、タスクを並行して実行することができます。これにより、長時間実行されるタスクをバックグラウンドで開始して完了させながら、メインプログラムを実行し続けることができます。

JavaScriptのコールバック

JavaScriptの世界では、関数はアプリケーションの振る舞いを作り上げる上で重要な役割を果たします。関数の多様な性質により、関数は言語内の他の値と同様に扱われることができます。特にコールバックは、別の関数の引数として渡され、後で実行されることが期待される関数です。

関数を値として渡し、後で実行することができる能力は、さまざまな可能性を開くものです。コールバックは、操作が完了した後に実行されるべきカスタムの振る舞いを提供したり、特定の条件が満たされた時に実行されたりするために使用することができます。

JavaScriptでのコールバックの実装

簡単な例を考えてみます。データベースからデータを取得するなど、結果を計算するのに時間がかかる関数があるとします。この関数が実行されている間にプログラムが待機して何もしないことは避けたいため、コールバックを使用します。

以下はコードの例です。

js
function fetchData(callback) {
  // Simulate a delay with setTimeout
  setTimeout(function() {
    let data = 'Hello, world!';
    callback(data);
  }, 2000);
}

function printData(data) {
  console.log(data);
}

// Call fetchData and pass in printData as the callback
fetchData(printData);

この例では、fetchDataはコールバック関数を引数として受け取ります。setTimeoutを使用して遅延をシミュレートしてデータを取得します。データが準備できたら、データを引数としてコールバック関数を呼び出します。printData関数はコールバックとして渡されており、データが準備されると呼び出され、データをコンソールに出力します。

コールバック地獄の問題

コールバックは単純な場合には便利ですが、コールバックをネストして使用すると複雑になることがあります。この状況は一般的に「コールバック地獄」と呼ばれます。

例えば、データベースからデータを取得し、そのデータを基にさらにデータを取得し、それに基づいて別の操作を行う必要がある場合、コールバックがネストされることになります。

js
fetchData(function(data) {
  fetchMoreData(data, function(moreData) {
    doSomethingWithTheData(moreData, function(result) {
      console.log(result);
    });
  });
});

コードはピラミッドのようになり、読みやすさと保守性が失われる可能性があります。これは、プロミスとAsync/Awaitが解決しようとする主要な問題の1つです。

JavaScriptのプロミス

コールバックによって引き起こされる課題に対応するために、特に「コールバック地獄」として知られる状況に対してJavaScriptでプロミスが導入されました。プロミスは非同期操作の完了または失敗を表すオブジェクトです。プロミスは非同期操作を扱うための堅牢な方法を提供し、コールバックの深いネストを回避します。

プロミスの状態とライフサイクル

プロミスは次の3つの状態のいずれかに存在することができます。

-保留中(Pending)
プロミスの結果はまだ決まっていません。結果が得られる非同期操作がまだ完了していないためです。

  • 充足済み(Fulfilled)
    非同期操作が完了し、プロミスに結果が存在します。
  • 拒否済み(Rejected)
    非同期操作が失敗し、プロミスは充足されません。拒否された状態では、プロミスにはなぜ操作が失敗したかを示す理由が存在します。

重要な点として、プロミスが充足または拒否されると、その状様に変更することはありません。プロミスは充足または拒否された後は状態を変更することはありません。プロミスは、充足または拒否されている場合、決定済みと言います。

プロミスの作成と消費

プロミスはnew Promiseコンストラクタを使用して作成されます。Promiseコンストラクタは、resolverejectという2つの引数を取る関数、エグゼキュータと呼ばれる関数を取ります。resolvereject引数は、それぞれプロミスを決定させるための関数です。resolveは値でプロミスを充足し、rejectは理由でプロミスを拒否します。

以下はプロミスを作成する簡単な例です。

js
let myFirstPromise = new Promise((resolve, reject) => {
  let condition = true; // this could be the result of some asynchronous operation

  if(condition) {
    resolve('Promise is fulfilled!');
  } else {
    reject('Promise is rejected!');
  }
});

プロミスを消費するには、thenメソッドを使用します。thenメソッドは2つのオプション引数を取ります。成功時のコールバックと失敗時のコールバックです。これらのコールバックはハンドラとも呼ばれます。

以下は上記で作成したプロミスを消費する例です。

js
myFirstPromise
  .then(successMessage => {
    console.log(successMessage);
  })
  .catch(errorMessage => {
    console.error(errorMessage);
  });

この例では、プロミスが充足された場合は成功メッセージがコンソールにログ出力されます。プロミスが拒否された場合はエラーメッセージがコンソールにログ出力されます。

プロミスのチェーン

プロミスのもっとも強力な機能の一つは、それらをチェーンすることができる点です。つまり、前の操作が完了したときに次の操作を連続して実行できるということです。

以下はプロミスのチェーンの例です。

js
fetchData()
  .then(data => {
    console.log(data);
    return fetchMoreData(data);
  })
  .then(moreData => {
    console.log(moreData);
    return doSomethingWithTheData(moreData);
  })
  .then(result => {
    console.log(result);
  })
  .catch(error => {
    console.error(error);
  });

この例では、それぞれのthenメソッドが新しいプロミスを返し、プロミスのチェーンを形成しています。チェーン内のどのプロミスでも拒否された場合、制御はもっとも近いcatchメソッドに移ります。これにより、プロミスチェーンでのエラーハンドリングが非常に簡単になります。

Async/Await

Async/AwaitはJavaScriptで非同期操作を扱うための近代的な方法です。ES2017で導入され、Async/Awaitは基本的にプロミスを基にした構文糖衣であり、プロミスをより見やすく、理解しやすく扱えるようにします。

JavaScriptでのAsync/Awaitの使用

Async/Awaitを使用するには、asyncキーワードを使って非同期関数を定義します。非同期関数の内部では、awaitキーワードを使ってプロミスの前に実行を一時停止し、プロミスが解決または拒否されるのを待ちます。

以下は例です。

js
async function fetchAndDisplayData() {
  try {
    let data = await fetchData();
    console.log(data);

    let moreData = await fetchMoreData(data);
    console.log(moreData);

    let result = await doSomethingWithTheData(moreData);
    console.log(result);
  } catch(error) {
    console.error(error);
  }
}

fetchAndDisplayData();

この例では、fetchDatafetchMoreDatadoSomethingWithTheDataは全てプロミスを返す関数です。これらの関数呼び出しの前にawaitキーワードがあり、JavaScriptはプロミスが解決または拒否されるまで実行を待ちます。つまり、fetchAndDisplayDataは非同期操作を含んでいるにも関わらず、同期的な関数のように振る舞います。

Async/Awaitでのエラーハンドリング

非同期関数でのエラーハンドリングは、同期コードと同様に、try/catchブロックを使用して行います。上記の例では、プロミスのいずれかが拒否された場合、実行はcatchブロックに移ります。

Ryusei Kakujo

researchgatelinkedingithub

Focusing on data science for mobility

Bench Press 100kg!