JavaScriptの非同期の基礎

jsコード JavaScript

本記事では、JavaScriptの学習項目の中でも最高難易度を誇る「非同期」について、基礎的な内容から学んでいきます。

非同期とは

JavaScriptの非同期とは、複数のタスクを同時に進行できるようにするためのプログラミング手法です。
JavaScriptはシングルスレッドで実行されるため、通常は一度に1つの操作しか実行できません。
しかし、非同期処理を利用することで、ある操作が完了するのを待たず他の操作を続行できるようになります。

非同期処理は、特にウェブアプリケーションで重要です。
たとえば、サーバからデータを取得する際、ユーザーインターフェースが固まることなく、データの取得がバックグラウンドで行われます。
このように、ユーザーが入力を続けたり画面を操作する間に、裏側で非同期処理が行われることにより、スムーズなユーザーエクスペリエンスを提供します。

イベントループの基本概念

JavaScriptでは、複数の処理を同時に実行するために「イベントループ」という仕組みを使用します。

イベントループとは

イベントループは、非同期的に実行されるタスクを管理し、効率的に処理するためのメカニズムです。JavaScriptランタイム環境(例えば、ブラウザやNode.js)は、イベントループを使って非同期タスクを管理し、実行します。

イベントループの構成要素

それぞれの用語の意味を覚えておきましょう。

  1. コールスタック
    • 現在実行中のタスク(関数)とその呼び出し元の関数(親関数)が積み重ねられ、それらの実行状況を追跡・管理します。
    • タスク(関数)を「後入れ先出し」方式で管理しており、関数が呼び出されたらスタックの上に追加し(Push)、実行が終わるとスタックから除外されます(Pop)。
    • 現在実行中のタスクが完了すると、次のタスクがスタックから取り出されて実行されます。
  2. イベントキュー
    • 非同期タスクが完了すると、コールバック関数がこのキューに追加されます。
    • キューに追加されたコールバック関数は、コールスタックが空になったときに実行されます。
  3. Web API
    • タイマー(*1)、DOM操作(*2)、HTTPリクエスト(*3)など、ブラウザが提供する非同期APIです。
    • これらのAPIは非同期処理を実行し、その結果をイベントキューに追加します。

*1:setTimeout関数、setInterval関数がタイマーの具体例として挙げられます。

*2:querySelectorメソッド、getElementByIdメソッドなどがDOM操作の具体例として挙げられます。

*3:fetch関数、XMLHttpRequestオブジェクトなどがHTTPリクエストの具体例として挙げられます。

イベントループの動作原理

イベントループの基本的な動作は次の通りです:

  1. コールスタックが空であるかをチェックします。
  2. 空であれば、イベントキューから最初のタスクを取り出し、コールスタックにプッシュします。
  3. コールスタックが空になるまで、すべての同期タスクが順番に実行されます。
  4. コールスタックが空になると、イベントループはイベントキューからコールバック関数を取り出し、コールスタックに追加して実行します。
  5. コールバック関数の実行が完了すると、再びイベントキューから次のコールバック関数が取り出され、同様の手順で実行されます。

非同期の歴史的変遷

JavaScriptにおける非同期処理は、初期のシンプルなコールバック関数から、より複雑で強力なPromiseに、そして2024年12月現在での最終形態である async/await へと進化してきました。

ここでは、その歴史的変遷を振り返り、各段階での特徴と利点について説明します。

黎明期:コールバック

初期のJavaScriptでは、非同期処理は主にコールバック関数を使って実装されていました。
コールバック関数とは、特定のタスクが完了したときに呼び出される関数です。

コールバックのサンプル
function fetchData(callback) {
    setTimeout(() => {
        const data = 'Fetched Data';
        callback(data);
    }, 1000);
}

fetchData((data) => {
    console.log(data); // "Fetched Data" (1秒後)
});

コールバック関数はシンプルで直感的ですが、複数の非同期操作を連続して行う場合、ネストが深くなります。
すると、コードの可読性が低下し、デバッグやメンテナンスが困難になるという問題が起きます。

この問題は「コールバック地獄(Callback Hell)」と呼ばれました。

ルネッサンス:Promise の誕生

前述の「コールバック地獄(Callback Hell)」の問題を解決するために、Promiseが導入されました。Promiseは、非同期処理の結果を表すオブジェクトであり、成功(resolve)または失敗(reject)を処理できます。

Promiseのサンプル
function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('Fetched Data');
        }, 1000);
    });
}

fetchData().then((data) => {
    console.log(data); // "Fetched Data" (1秒後)
}).catch((error) => {
    console.error('Error:', error);
});

Promiseには、コールバックと比較して次のような特徴があるため、コードの見通しがよくなる(可読性が向上する)利点があります。

  • 非同期操作をチェーンとして扱うことができる
  • エラーハンドリングが一箇所に集約される

Promise については、独立した記事『JavaScriptのプロミス』で詳細に解説しています。

最先端: async/await

Promiseだけでも効果的ですが、複雑な非同期フローを管理する際には依然として分かりづらくなる場合もあります。

飽くなき進化を求めるJavaScriptは、よりシンプルにPromiseを扱える async/await 構文を作り出しました。

async/awaitを使用したfetchのサンプル
async function fetchData() {
  const response = await fetch('https://swgoh-chiptips.jp/test/api/data.json');
  const data = await response.json();
  console.log(data);
}

fetchData();
console.log("Hello");

👆このプログラムでは、非同期関数 fetchData が呼び出されますが、await キーワードを使っているため、内部の fetch 処理が完了するのを待つ必要があります。
しかし、fetchData 関数自体の呼び出しは非同期(async)であり、その完了を待たず次の行の console.log("Hello") が先に実行されます。

つまり、このプログラムの実行順序は以下の通りです:

  1. fetchData 関数が呼び出される。
  2. fetch リクエストが送信されるが、その完了を待たずに次の行へ進む。
  3. console.log("Hello") が実行され、「Hello」がコンソールに表示される。
  4. fetch リクエストが完了し、response が返される。
  5. response.json() が実行され、データが変数 data に格納される。
  6. console.log(data) が実行され、取得したデータがコンソールに表示される。

async/await については、独立した記事『JavaScriptのasync/await』で詳細に解説しています。

並行処理を実現する Web Workers

Web Workersは、非同期処理の一環として「並行処理」を可能にする技術として登場しました。
これにより、バックグラウンドで重い処理を実行しつつ、メインスレッドがブロックされることなくUIが動作し続けることができます。

Web Workers の特性

  • バックグラウンド処理
    • メインスレッドに影響を与えることなく重い処理を実行できる。
  • 独立したスコープ
    • メインスレッドと分離された実行環境で動作する。
  • メッセージング
    • メインスレッドとデータをやり取りするために postMessage メソッドと onmessage イベントを使用する。

Web Workersの基本的な使用方法

以下に、メインスレッドとWeb Workersの基本的な使用方法の例を示します。

メイン・スレッド(main.js)
// worker.js を新しい Web Worker として生成
const worker = new Worker('worker.js');

// Worker からのメッセージを受け取るイベントハンドラー
worker.onmessage = function(event) {
    console.log('Message from worker:', event.data);
};

// Worker にメッセージを送信
worker.postMessage('Hello, worker!');
ワーカー・スレッド(worker.js)
// メインスレッドからのメッセージを受け取るイベントハンドラー
onmessage = function(event) {
    console.log('Message from main thread:', event.data);
    // メインスレッドにメッセージを送信
    postMessage('Hello, main thread!');
};

👆この例では、メインスレッドからWeb Workerにメッセージを送信し、Web Workerがそれに応答します。これにより、メインスレッドとWeb Worker間でデータのやり取りができます。

【上記サンプルプログラムの注意事項】

  • 上記のサンプルプログラムを実行するには、ウェブサーバが必要です。ローカル環境で実行する場合は、apacheやnginx、Node.jsのhttp-serverやPythonのシンプルHTTPサーバーなどを使用してください。
  • サンプルプログラムを実行すると、Web Workerがバックグラウンドで動作し続けます。メインスレッドに影響を与えることはありませんが、ブラウザを閉じるまでWeb Workerは停止しませんので注意してください。

Web Workers の制限

並行処理に欠かせないWeb Workersですが、使用する際には次の制限に注意する必要があります。

  • 制限された環境:
    • Web WorkersはDOM操作ができず、ウィンドウオブジェクトにもアクセスできません。
  • メッセージングのオーバーヘッド:
    • メインスレッドとWeb Worker間の通信にはオーバーヘッドがあり、データのコピーが発生します。
  • リソース消費:
    • 複数のWeb Workersを生成すると、システムリソースを多く消費する可能性があります。

Web Workers の実用的なシナリオ

Web Workersの使いどころとしては、以下のようなシナリオで特に有用です。

  • データ処理:
    • 大量のデータをバックグラウンドで処理し、結果をメインスレッドに返す。
  • リアルタイムアプリケーション:
    • ゲームやチャットアプリケーションなど、リアルタイム性が求められるアプリケーションでのバックグラウンド処理。
  • 計算集約型タスク:
    • 複雑なアルゴリズムや数学的計算をWeb Workersで実行し、メインスレッドをブロックしない。

まとめ

この記事では、JavaScriptの非同期処理について基礎的な内容を詳しく解説しました。
非同期処理は、JavaScriptプログラミングにおいて非常に重要な概念であり、特にウェブアプリケーションのパフォーマンスとユーザーエクスペリエンスの向上に欠かせません。

本記事で扱った内容をまとめると次の通りです。

  1. 非同期とは
    • 複数のタスクを同時に進行できるプログラミング手法であり、ユーザーインターフェースが応答し続けるために重要です。
  2. イベントループの基本概念
    • JavaScriptランタイム環境で非同期タスクを管理するメカニズムであり、コールスタック、イベントキュー、Web APIの役割を理解することが不可欠です。
  3. 非同期の歴史的変遷
    • コールバック関数から始まり、Promise、そして現在の主流であるasync/awaitへと進化してきました。各手法にはそれぞれの特性と利点があり、適切に使い分けることで効率的な非同期処理が可能になります。
  4. 並行処理を実現するWeb Workers
    • 重い処理をバックグラウンドで実行し、メインスレッドのパフォーマンスを向上させるための技術です。Web Workersの特性、使用方法、制限、実用的なシナリオを理解することで、より複雑なアプリケーションを構築できます。

これらの知識を活用することで、JavaScriptの非同期処理を効果的に利用し、より快適で応答性の高いアプリケーションを開発することができます。
非同期処理は初めて触れると難しく感じるかもしれませんが、基本的な概念と実例を理解することで、確実に習得できる分野です。

今後の開発において、この記事が役立つことを願っています。


本記事についての質問、誤りの指摘、ご意見ご感想などありましたら、ぜひコメント頂ければ幸いです。

最後までお読みいただき、ありがとうございます。

『詳説 JavaScript』メニューに戻る

コメント

タイトルとURLをコピーしました