JavaScriptの非同期処理をメモリ領域から解説!サンプルコードも掲載

本ページはプロモーションが含まれています

「非同期処理の仕組みを理解したい」

「自分の手で実装してみたい」

JavaScriptの基礎を固めた人は、実際のWebアプリ開発で用いられる非同期処理に触れて「難しくて私にはできないかも・・・」と思っているのではないでしょうか。

参考書やドキュメントには、API連携といった結論だけが書かれていることが多く、難しく感じるのも当然です。

非同期処理を基礎から理解するには、メモリ(すぐ使うデータを一時的に置いておく場所)が役割ごとに分かれて動いている仕組みを知る必要があります。

そこで、本記事ではWebアプリの開発経験があるみずがめが、以下の内容を紹介します。

  • 非同期処理と関係する3つのメモリ領域
  • ブラウザサービスで実行できるサンプルコード

本記事を最後まで読むことで、非同期処理の仕組みと流れをイメージできるようになります。

記事後半では、オンラインエディタ(プログラムコードを実行できるサイト)で実際に動作するコードを載せているため、実際に手を動かして勉強に役立ててみてください。

非同期処理は、複数のメモリ領域が連動してWebアプリの処理が進むことを指します。

メモリとは、すぐ使うデータを一時的に置いておく作業机のようなものです。

メモリ領域とは、パソコンのメモリの中に、特定の目的のために確保された「専用の区画」を指します。

Google ChromeやFirefoxなどのブラウザを起動すると、以下に示す3つのメモリ領域がユーザーのパソコンに作られます。

メモリ領域役割
Call Stack関数の実行を記録
Web APIsWeb APIの受付窓口
Task QueueWeb APIで処理されたデータを待機させる

最初に「Call Stack」で処理が始まり、その一部が「Web APIs」に仕分けされます。「Web APIs」の処理が終わると、「Task Queue」を経由して「Call Stack」に合流するのが非同期処理です。

1.Call Stack

「Call Stack」は、実行中の関数が終わった後、どこの関数(どの行)へ進むのか記録するメモリ領域です。

以下のルールがあります。

  • 実行中の関数が記録される
  • 実行が終わると記録が即削除される
  • 関数の中で別の関数を呼び出している場合は連動して記録される
  • 関数内で呼び出された関数が優先的に処理される

「Call Stack」は、後から入ってきた関数を先に処理する「積み上げ」というルールがあります。

たとえば、おおもとの関数をA、その中で呼び出されている関数をBとしましょう。

「Call Stack」内では、Aの上にBが積み上がった状態(優先順位がB)で記録されます。Bが処理されるとAの処理(後回しにされたもの)が再開されます。

処理する順番を整理するのが「Call Stack」の役割です。

みずがめ
みずがめ

積み上げは、新しい本を上に積み重ねていき、一番上にある本から読む(処理)と捉えるといいでしょう!

2.Web APIs

「Web APIs」は、ブラウザ(ChromeやSafariなど)に組み込まれている機能群です。

JavaScriptだけの場合、計算や文字列の編集といったシンプルなことしかできません。

タイマー機能や、DOM操作などは、ブラウザが機能を提供(処理を担当)してくれることで成り立ちます。JavaScript単体では扱えない機能を補ってくれるのが「Web APIs」です。

みずがめ
みずがめ

役割としては、「Call Stack」が総合窓口、「Web APIs」が専門で業務を請け負う部門というイメージです!

実際の動作では、「Call Stack」でブラウザ機能のサポートが必要と判断された関数は、「Web APIs」へ処理が移ります。

「Web APIs」の処理が終わると、「Task Queue」というメモリ領域にデータが送られます。その後、「Web APIs」から実行履歴が削除されます。

3.Task Queue

「Task Queue」は、「Web APIs」で処理が終わった関数を待機させておく場所です。

「Event Loop」と呼ばれるプログラムが、常に「Task Queue」と「Call Stack」を監視しており、以下の条件を満たしたときに、関数を移動させます。

  • Task Queueに関数が待機している
  • Call Stackが完全に空である(処理するものがない)

「Call Stack」と違い、待機している順番に関数を処理していくのが特徴です。

「Call Stack」「Web APIs」「Task Queue」が連動することで、Webアプリの動作を支えています。

JavaScriptで非同期処理を実装するには、以下に示す4つの構文を理解する必要があります。

  1. setTimeout()
  2. Promise
  3. async
  4. await

「Promise」は、非同期処理をスムーズに実行させるものです。以降の「async」や「await」につながるため、しっかり取り組んでみてください。

また、すべてmyCompilerで実行できます。コピペで張り付けて実行結果を確認していくとより深く理解できます。ぜひご活用ください。

1.setTimeout()

「setTimeout()」は非同期処理のコードで、「Web APIs」に時間を測るよう依頼します。

基本的な書き方は「setTimeout(() => {}, ミリ秒);」です。

サンプルコードは以下のとおりです。

console.log("--- 処理スタート ---");

// 1. Web APIsに「10秒数えて、この中身を実行する」と依頼する

setTimeout(() => {

  // 10秒後に、Task QueueからCall Stackに渡されて実行される

  console.log("② 10秒経ちました!(後回しにされた処理)");

}, 10000);// 10000ミリ秒(10秒)待つ

// 2. 依頼した直後、待たずにこの行へ進む

console.log("① 10秒待たずに、この行を先に実行しました。");

このコードでは「setTimeout()」でWeb APIsに10秒のタイマー処理を依頼しました。10秒数えている間「Call Stack」では、別の関数やコードを実行し続けています。

そして、空になったタイミングでカウントが終わった「console.log(“② 10秒経ちました!(後回しにされた処理)”);」が表示されます。

しかし、10秒ぴったりに処理されるとは限りません。

関数が「Call Stack」に積まれている状態(未処理の関数がある)だと「Task Queue」から「Call Stack」に移動できないため、カウントを終えていてもすぐ実行されません!

この不安定な要素を解決するのが「Promise」です。

2.Promise

「Promise」は、非同期処理の状態を確定させ、次の処理を確実に実行させる仕組みです。

3つの状態があり、それぞれ処理が異なります。

Promiseの3つの状態状態の意味起動条件次のアクション
PendingWeb APIsでの処理が終わっていない初期状態Web APIsが処理を担当している
Call Stackは別の関数を処理し始める
Fulfilled(解決済み)Web APIsで処理され、Call Stackで実行が終わったresolve()がオンになる.then()の中へ処理が移動
Rejected(拒絶済み)Web APIsでの処理がうまくいかない、またはデータのチェックで弾かれるreject()がオンになる.catch()の中へ処理が移動

状態は「Pending」から「Fulfilled」または「Rejected」のどちらかになった時点で固定され、以降の処理がスムーズになります。

非同期処理の中には、いつ終わるのか、何回実行されるのかわからないコードがあり、次の処理へ進むタイミングが不安定になりがちです。

「Promise」は、3つのどれかに状態を確定させて、処理を進めてくれます。

みずがめ
みずがめ

「解決した」または「失敗だった」という報告だけしておくイメージです!

「Promise」は「return new Promise()」と書き始めるのが一般的です。

「return new Promise」は、中にある「resolve()」を起動させ、関数の外にある「.then()」にデータを渡します。

「reject()」を起動した場合は、「.catch()」にデータを渡して処理が続きます。

「.then()」と「.catch()」はデータを渡された時点で確実に実行されるコードです。次に処理される関数が決まるため、処理が遅くなるといった問題が起きにくくなります。

以下のサンプルコードを実行してみてください。

const startCoding = () => {

  return new Promise((resolve) => {

    console.log("① Web APIsで10秒数え始める");

    setTimeout(() => {

      // 10秒後にPromiseを成功状態にするスイッチが入る

      resolve("③ 10秒経つと、Promiseは成功(Fulfilled)状態に変わる");

    }, 10000);

  });

};

// 2. 実行する

console.log("---この部分が一番上に表示される---");

startCoding().then((data) => {

  // resolve() が実行されると、.then()が起動する。(data)はresolve()の中を受け取る

  console.log(data);

  console.log("--- すべて完了 ---");

});

console.log("② 10秒経過する前に、この行を実行します。");

3.async

「async」は、「return new Promise」を簡潔に書くための構文です。

以下のコードと組み合わせて使います。

asyncと組み合わせるコードコードの意味実行中の扱い状態の変化その後
return処理成功resolve()として処理されるFulfilled(解決済み)に変わる次の行に進む
throw処理失敗reject()として処理されるRejected(拒絶済み)に変わるcatch ブロックへ強制ジャンプ

たとえば、関数内で正常に「return」が機能すると、自動的に「resolve()」として処理され、状態が「Fulfilled(解決済み)」になります。

処理に失敗して「throw」が実行された場合、「reject()」として扱うため、「Rejected(拒絶済み)」に固定されます。

「async」は、「Promise」をより簡潔に書くためのものです。

以下にサンプルコードを掲載します。

async function testChat() {

  return "最後に処理されているところはasyncを使っている";

}

// 2. 実行(awaitなし)

console.log("①処理開始");

testChat().then((result) => {

  // ここはthen()なので「後回し」にされる。Promiseが解決した後に動く

  console.log("③ async実行結果: " + result);

});

console.log("② .then部分は無視されて、この行(console.log)が実行される");

4.await

「await」は「async関数」の中だけで使える構文です。「Promise」の状態が「Pending(実行中)」の間、「await」が書かれた箇所で、Promiseの完了を待機します。

また、「Fulfilled(解決)」した瞬間に、自動で「resolve()の値」を取り出して変数に代入してくれる便利なものです。

「.then()」を書く必要がないため、一般的な関数に近い形で扱えます。

サンプルコードを掲載します。

async function testChat() {

  // 本来はここで通信などする。今回はシンプルに値を返すだけ

  // この return はresolve() と同じ

  return "来年の抱負";

}

// 2. 実行する関数

async function main() {

  console.log("① 処理を開始する合図");

  // await で testChat() 関数の return (resolve) を待機

  const data = await testChat(); 

  // 解決した瞬間にここへ進む

  console.log("② " + data); 

}

main();

「setTimeout()」はカウントするだけ、「await」は「Promise」の完了を待つための構文です。

非同期処理は似ている構文が多いため、違いがわかるまで手を動かして練習してみてください。

コメント

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