トップへ戻る
BLOGS

JavaScript 非同期処理の基礎

JavaScript 非同期処理の基礎

非同期処理の概要

スレッド

JavaScriptは処理の流れスレッドという概念で実行しています。
スレッドとは処理の一連の流れのことで、コードが上から実行されていく様を一本の糸のように表すことができることを「一つのスレッドでコードが実行されている」と表現します
そのため、JavaScriptは基本的にシングルスレッドです

JavaScriptにおける非同期処理とは、メインスレッドから切り離された処理のことです

コールスタック

実行コンテキストが積み重なって出来たものをコールスタックと呼びます。
コードが実行されるときには必ず実行コンテキストが生成されるため、コールスタックには必ずコンテキストが積まれている状態となります
コールスタックが空でない場合にはメインスレッドが使用中であるということですね

タスクキュー

実行待ちのタスク(非同期での実行が予約されている関数)が格納されているキューのこと
非同期処理の関数はまずここに格納されます

イベントループ

タスクキューに格納されたタスクを順番に実行していく仕組みです
イベントループは定期的にコールスタックを監視しコールスタックが空になった時にタスクキューから一番古いタスク(関数)を取り出して実行します

Promise

Promiseのインスタンス化

Promiseで非同期処理をハンドリングするにはまずnew Promiseとしてインスタンス化します
そしてその時に引数に渡すコールバック関数内に非同期処理を記述します

let プロミスインスタンス = new Promise(非同期処理を扱う関数);
const instanse = new Promise(asyncHandler);
function asyncHandler(resolve, reject) {
  setTimeout(() => {
    if(非同期処理は成功?) {
      resolve(data);
    } else {
      reject(error);
    }
  })
}

コールバック関数asyncHandlerの2つの引数resolve,reject(変数名は任意)にはJavaScriptによって特別な関数が渡されます。これらの引数は非同期処理の実行後の状態によって次のように使い分けます

非同期処理が成功した時
非同期処理が成功した時はresolveを実行します。resolveが呼び出されるとPromiseインスタンスのthenメソッドで登録した処理(コールバック関数)が実行されます
非同期処理が失敗した時
非同期処理が失敗したときはrejectを実行します。rejectが呼び出されるとPromiseインスタンスのcatchメソッドで登録した処理(コールバック関数)が実行されます

そのため以降で紹介するthenメソッドには成功時の、catchメソッドには失敗時の処理をそれぞれ記述していくことになります

thenメソッド

let thenProm = プロミスインスタンス.then(successHandler);
function successHandler(data) {
 非同期処理が成功した時の処理
}

返り値のthenPromsuccessHandlerの処理が登録されたプロミスインスタンスとなります

thenメソッドのコールバック関数successHandlerresolveが呼び出されると実行されます。
またresolve(data)でわたした引数dataはコールバック関数successHandlerの引数として渡されてきます。

thenメソッドが実行されると戻り値として更にthenProm(プロミスインスタンス)が返ります。
これに対してさらにthenメソッドをつなげていく事によって複数の非同期処理を順番に実行することも出来ます。いわゆるthenチェーンってやつです

ちなみにthenメソッドの第二引数を設定した場合は、非同期処理が失敗した時のcatchメソッドのコールバック関数と同じ意味になります

let instance = new Promise(...).then(成功した時,失敗した時);

catchメソッド

let catchProm = プロミスインスタンス.catch(errorHandler);
function errorHandler(error) {
 非同期処理が失敗した後の処理
}

catchメソッドのコールバック関数errorHandlerrejectが呼び出されたタイミングで実行されます。
またreject(error)でわたした引数errorはコールバック関数の引数(data)として渡されてきます。

finallyメソッド

finallyメソッドに記述するの非同期処理が完了した時に必ず行いたい処理です
つまりthenまたはcatchのコールバック関数の処理が完了した後に必ず実行されます

let finallyProm = プロミスインスタンス.finally(finallyHandler);
function finallyHandler() {
 非同期処理が成功、失敗に関わらず必ず行いたい処理
}

例えばサーバーへの通信中に画面上にローダーを表示していた場合などで、成功、失敗に関わらずローダーの表示をやめたい場合などにfinallyメソッドにその処理を記述するわけですね

Promiseチェーン

上記の文法が基本ですが例文のようにいちいちインスタンス化することは実務ではしないようです、普通はチェーンメソッドと呼ばれる記法でメソッドをつなげて記述します

let instance = new Promise(...).then(...).catch(...).finally(...);

上で解説したとおりすべてのPromiseメソッドは返り値としてPromiseインスタンスを返すからです

この記法をしっかり覚えて非同期処理を直列で実行する方法を学びましょう
ここでの直列とは前の非同期処理の完了を待って、次の非同期処理の実行を行うことです

そうして順番に非同期処理を行っていくことをPromiseチェーンと呼ぶそうです

非同期処理を直列で実行するときには、thenメソッドの戻り値にPromiseインスタンスを設定します

また、Promiseチェーン内のいずれかのPromiseインスタンスでrejectが呼び出されるとcatchメソッドに処理が移行します

以下は例です

function promiseFactory(count) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      count++;
      console.log(
        `${count}回目のコールです。時刻:[${new Date().toTimeString()}]`
      );
      if (count === 3) {
        // 3回目のコールでエラー
        reject(count);
      } else {
        resolve(count);
      }
    }, 1000);
  });
}
promiseFactory(0)
  .then((count) => {
    return promiseFactory(count);
  })
  .then((count) => {
    return promiseFactory(count);
  })
  .then((count) => {
    return promiseFactory(count);
  })
  .then((count) => {
    return promiseFactory(count);
  })
  .catch((errorCount) => {
    console.error(`エラーに飛びました。現在のカウントは ${errorCount} です。`);
  })
  .finally(() => {
    console.log("処理を終了します。");
  });
// > 1回目のコールです。時刻:[16:23:19 GMT+0900 (日本標準時)]
// > 2回目のコールです。時刻:[16:23:20 GMT+0900 (日本標準時)]
// > 3回目のコールです。時刻:[16:23:21 GMT+0900 (日本標準時)]
// > エラーに飛びました。現在のカウントは3です。
// > 処理を終了します。

上記のコードではPromiseインスタンスを返す関数promiseFactoryを作成しています
そのため、promiseFactoryを実行すると、新しいPromiseインスタンスが戻り値として取得されます

また、thenメソッドのコールバック関数のreturnに続けてpromiseFactoryを実行しているためまた再びPromiseインスタンスが取得されます

注意点としてはPromiseコンストラクタに渡す(resolve, reject) => {...}のコールバック関数はnew Promise(インスタンス化)を実行した時に実行されるため、一度インスタンス化して変数にした後チェーンをつなげようとすると上手く行かないのでご留意を

Promiseの状態管理

Promiseのインスタンスは、内部で現在の状態(ステータス)を管理、これによって自身のメソッド(thenやcatch)などの呼び出しを制御しています
以下はステータスの一覧です

Promiseステータスの一覧

pending
resolve、rejectが呼び出される前の状態
fulfilled
resolveが呼び出された状態
rejected
rejectが呼び出された状態

これまで「resolveが呼び出されるとthenに処理が移る」と説明してきましたが、これはPromiseインスタンス内部で保持しているステータスがfulfilledの状態に移行したことを表します

一方、rejectが実行されたときには、ステータスがrejectedとなります
fulfilledまたはrejectedに移行する前の状態をpendingと言います

またfulfilledrejectedのいずれかに遷移した状態はsettledと言います
Promiseステータスの確認はPromiseインスタンスをコンソールに出力すると確認できます

Promiseを使った並列処理

Promise.all

すべての非同期処理を並列に実行し、すべての完了を待ってから次の処理を行いたい場合にはPromise.allを使います

Promise.all(iterablePromises)
.then((resolveArray) => {...})
.catch((error) => {...});

iterablePromises:
Promiseインスタンスを含む反復可能オブジェクト(配列など)を設定します

resolveArray:
iterablePromisesに格納された各Promiseのresolveの実引数が格納された配列となって渡されてきます。

error:
最初にrejectedになったインスタンスのrejectの引数が渡ってきます

Promise.allは引数で与えられた反復可能オブジェクトに格納されている、すべてのPromiseインスタンスの状態がfulfilledになった時に、Promise.allに続くthenに処理を移行します。またいずれかのPromiseインスタンスのステータスがrejectedになった時にはcatchメソッドに処理が移ります

下記は簡単な用例です

function wait(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log(`${ms}msの処理が完了しました。`);
      resolve(ms);
    }, ms); // 引数のms分だけ待機
  });
}
const wait400 = wait(400);
const wait500 = wait(500);
const wait600 = wait(600);
Promise.all([wait500, wait600, wait400]).then(
  ([resolved500, resolved600, resolved400]) => {
    console.log("すべてのPromiseが完了しました。");
    console.log(resolved500, resolved600, resolved400);
  }
);

上記の例を実行すると、コールバック関数でわたしたすべての処理が終わってからthenメソッドの移行しているのが確認できます。つまり
Promise.allメソッドに渡した配列内のすべてのPromiseインスタンスがfulfilledになると、Promise.allに続くthenメソッドに処理が移ります

その際、thenのコールバック関数の引数に渡されるのは、各resolveに設定した実引数が格納された配列です
またこの処理はあくまで並列で行っているため、Promiseチェーンを使った直列処理とは異なります
直列の場合には前の処理の完了を待ってから次の処理が実行されますが、並列の場合にはすべての処理が一斉に実行されます。状況によって使い分けるようにしましょう

Promise.race

Promise.raceは複数のPromiseインスタンスのいずれかの状態がsettledfulfulledまたはrejected)になった時に、Promise.raceにつづくthenまたはcatchメソッドへ移行し実行します

Promise.rece(iterablePromises)
.then((firstResolveValue) => {...})
.catch((error) => {...});

iterablePromises:
Promiseインスタンスを含む反復可能オブジェクト(配列など)を設定します

firstResolveValue:
最初にfulfilledになったインスタンスのresolveの引数の値が渡ってきます

error:
最初にrejectedになったインスタンスのrejectの引数が渡ってきます

// resolve()を100ミリ秒後に実行するPromiseインスタンス
const myResolve = new Promise((resolve) => {
  setTimeout(() => {
    resolve("resolveが呼ばれました。");
    console.log("myResolveの実行が終了しました。");
  }, 100);
});
// reject()を200ミリ秒後に実行するPromiseインスタンス
const myReject = new Promise((_, reject) => {
  setTimeout(() => {
    reject("rejectが呼ばれました。");
    console.log("myRejectの実行が終了しました。");
  }, 200);
});
Promise.race([myReject, myResolve])
  .then((value) => {
    console.log(value);
  })
  .catch((value) => {
    console.log(value);
  })
  .finally(() => {
    console.log("finallyが呼ばれました。");
  });
// > myResolveの実行が終了しました。
// > resolveが呼ばれました。
// > finallyが呼ばれました。
// > myRejectの実行が終了しました。

上記の例を実行するすると最初はsetTimeoutで設定した時間の短いmyResolveが呼ばれます。
この時myResolveに格納されるPromiseインスタンスのステータスがfulfilledになります

いずれかのインスタンスがfulfilled(またはrejected)になった時点で後続処理のthenメソッドが呼び出されます。この時渡される引数(value)は変化があったインスタンスの実引数です。

myRejectのsetTimeoutで設定した時間で処理が実行されています
もちろんmyRejectの内部ではrejectが呼び出されていますが、これによる後続の処理(catchメソッドのコールバック関数)の実行は発生しません

上記の例は設定時間を逆にしてやると理解しやすいです

Promise.any

Promise.anyは複数のPromiseインスタンスのいずれかの状態がfulfilledになったタイミングでthenメソッドに処理を移行します。
また全てのインスタンスの状態がrejectedになった時に、catchメソッドを実行します

Promise.any(iterablePromises)
.then((resolvedValue) => {...})
.catch((error) => {...});

iterablePromises:
Promiseインスタンスを含む反復可能オブジェクト(配列など)を設定します

resolvedValue:
最初にfulfilledになったインスタンスのresolveの引数の値が渡ってきます

error:
AggregateErrorという特殊なオブジェクトです

// resolve()を200ミリ秒後に実行するPromiseインスタンス
const myResolve = new Promise((resolve) => {
  setTimeout(() => {
    resolve("resolveが呼ばれました。");
    console.log("myResolveの実行が終了しました。");
  }, 200);
});
// reject()を100ミリ秒後に実行するPromiseインスタンス(rejectがresolveより前に呼び出される)
const myReject = new Promise((_, reject) => {
  //resolveを使わないため_としておく;
  setTimeout(() => {
    reject("rejectが呼ばれました。");
    console.log("myRejectの実行が終了しました。");
  }, 100);
});
Promise.any([myReject, myResolve])
  .then((value) => {
    console.log(value);
  })
  .catch((value) => {
    console.log(value);
  })
  .finally(() => {
    console.log("finallyが呼ばれました。");
  });
// > myRejectの実行が終了しました。
// > myResolveの実行が終了しました。
// > resolveが呼ばれました。
// > finallyが呼ばれました。

myRejectが100ミリ秒後に実行されます。このインスタンスの結果はrejectedのため、Promise.anyは他のインスタンスの実行結果を待ちます

myResolveが200ミリ秒後に実行されています。 ここでmyResolveのステータスがfulfilledになったためthenメソッドへ処理が移行します

thenのコールバック関数の実行時に渡される引数はresolveの実引数の値となります

このようにPromise.anyではいずれかのインスタンスがfulfilledになるのを待ちます。またすべてのPromiseインスタンスがrejectedになった時catchメソッドへ移行しますが、この時渡される引数はAggregateErrorという特殊なエラーオブジェクトです

Promise.allSettled

Promise.allSettledはすべてのPromiseインスタンスの状態がsettled(fulfilledまたはrejected)になった時thenメソッドの処理を実行します

allSettledの場合はcatchメソッドは使いません。thenメソッドのコールバック関数にそれぞれのPromiseインスタンスの状態(status)と値(valueまたはreason)が対で格納された配列が渡されます

Promise.allSettled(iterablePromises)
.then((array) => {...})

iterablePromises:
Promiseインスタンスを含む反復可能オブジェクト(配列など)を設定します

array:
Promiseインスタンスの状態が対で格納された配列 以下は例です

[
 {status: "fulfilled", value: "resolveの値"},
 {status: "rejected", reason: "rejectの値"}
]

以下は使用する際の例文です

// resolve()を200ミリ秒に実行するPromiseインスタンス
const myResolve = new Promise((resolve) => {
  setTimeout(() => {
    resolve("resolveが呼ばれました。");
    console.log("myResolveの実行が終了しました。");
  }, 200);
});
// reject()を100ミリ秒に実行するPromiseインスタンス
const myReject = new Promise((_, reject) => {
  setTimeout(() => {
    reject("rejectが呼ばれました。");
    console.log("myRejectの実行が終了しました。");
  }, 100);
});
Promise.allSettled([myReject, myResolve]).then((arry) => {
  for (const { status, value, reason } of arry) {
    console.log(`ステータス:[${status}], 値:[${value}], エラー:[${reason}]`);
  }
});
// > myRejectの実行が終了しました。
// > myResolveの実行が終了しました。
// > ステータス:[rejected], 値:[undefined], エラー:[rejectが呼ばれました。]
// > ステータス:[fulfilled], 値:[resolveが呼ばれました。], エラー:[undefined]

myReject、myResolveの状態がsettledになるまで、後続のthenの処理を待機します

rejectedの場合にはreasonプロパティに対してrejectの引数の値が渡されます
fulfilledの場合にはvalueプロパティに対してresolveの引数の値が渡されます

Promise.allSettled内でエラーハンドリングを行いたい場合は、statusプロパティの値を確認して条件判定を加えるようにしましょう

Promiseの静的メソッド

Promise.resolveとPromise.reject

プロミスの静的メソッドです
それぞれの状態を持ったPromiseインスタンスを返します

使用するのはPromise.resolve位でしょうか?? 何らかの処理を非同期として実行したい場合になどお世話になるかもしれません

Promise.resolveとPromise.reject

let val = 0;
Promise.resolve().then(() => {
  // 非同期処理のためグローバルコンテキスト終了後に実行される
  console.log(`valの値は[${val}]です。`);
});
val = 1;
// 実行中のコンテキストを含むコールスタックが空になった
console.log("グローバルコンテキスト終了");
// > グローバルコンテキスト終了
// > valの値は[1]です。

Promise.reject("エラーの理由").catch(error => {
  console.error(error);
});
// 実行中のコンテキストを含むコールスタックが空になった
console.log("グローバルコンテキスト終了");
// > グローバルコンテキスト終了
// > エラーの理由

await / async

さてここからはさらに実践的、、むしろこっちをしっかり覚えましょうってなくらい現場で使用されている記法のようです

await / asyncはPromiseのthenの処理を簡潔に記述できるようになるためコードの視認性が上がります
この挙動を理解するためには上記のPromiseの理解が必須になるため、何回も書いてみましょう

async

asyncは関数の先頭につけることで非同期関数(AsyncFunction)という特殊な関数を定義することが出来ます

async function 関数名() {...};
もしくは、、
const 関数名 = ( async () => {...});

非同期関数の普通の関数との違いは、非同期関数のreturnが返す値は必ずPromiseインスタンスになります
つまり関数内で何をreturnしようともthenメソッドでつなぐことが可能です
これは非同期関数が呼び出された時に返り値がPromiseインスタンスであろうとなかろうと、自動的にPromiseインスタンスでラップされるからです。

await

awaitはPromiseインスタンスの前に記述することでPromiseのステータスがsettled ( fulfilledまたはrejected )になるまで後続コードの実行を待機します
もちろんawaitは非同期関数内でしか使えないため注意しましょう。

async function 関数名() {
 let resolvedValue = await prom
}

prom : Promiseのインスタンス
resolvedValue : Promiseインスタンス内でのresolveの実引数の値がawaitの結果として返ります

awaitはPromise内のresolveの実引数を取り出す役割もあります

const prom = new Promise((resolve) => {
  setTimeout(() => resolve("この値を取り出します。"), 1000);
});

async function asyncFunction() {
  const value = await prom;
  console.log(value);
}

asyncFunction();
// > この値を取り出します。

また仮にawaitで受けたPromise内でrejectが実行された場合はawaitは例外を発生させるため、
try...catch構文で使用することが出来ます

以下は上で書いていたPromiseチェーンのコードをawait / asyncで書き換えた例です

async / await 例文

// この関数はawait / asyncで書き換えることはできない
function promiseFactory(count) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      count++;
      console.log(
        `${count}回目のコールです。時刻:[${new Date().toTimeString()}]`
      );
      if (count === 3) {
        // 3回目のコールでエラー
        reject(count);
      } else {
        resolve(count);
      }
    }, 1000);
  });
}

// await / asyncを使った書き換え
async function execute() {
  // awaitを内部で使っているためasyncをつける
  try {
    // promiseFactory内のresolveが呼び出されるまで次の処理を実行しない
    //awaitによってresolveの引数の値がcountに代入される
    let count = await promiseFactory(0);
    count = await promiseFactory(count);
    count = await promiseFactory(count);
    count = await promiseFactory(count);
  } catch (errorCount) {
    // Promiseがrejectedのステータスになった場合はcatchブロックに遷移する
    console.error(`エラーに飛びました。現在のカウントは ${errorCount} です。`);
  } finally {
    console.log("処理を終了します。");
  }
}
// execute()の実行
execute();
// > 1回目のコールです。時刻:[01:13:26 GMT+0900 (日本標準時)]
// > 2回目のコールです。時刻:[01:13:27 GMT+0900 (日本標準時)]
// > 3回目のコールです。時刻:[01:13:28 GMT+0900 (日本標準時)]
// > エラーに飛びました。現在のカウントは3です。
// > 処理を終了します。

await / asyncの書き換えは「Promiseのthenの記述の簡略化」のために使用するためpromiseFactoryの書き換えは出来ません

上記のコードの書き換えでは、promiseFactoryの非同期処理をawaitで待機しています、awaitを使用する際は関数の前にasyncを付けます

awaitキーワードによってPromiseインスタンスの状態がsettledになるまで次の実行を待機しています

Promiseインスタンスの状態がrejectedになったときはcatchブロックに処理が移っています

Fetch

最後にサーバーからデータやファイルを取得する時に使うWeb APIの一種であるFetch API(fetch関数)について見てみます
fetch関数は非同期処理になるため取得したデータを使って処理をするには、上で学んできたPromiseやawait / asyncを使用する必要があります

まずは記法からです

fetch("リクエストURL"[,data])
.then((response) => response.json())
.then((data) => {取得したデータを使って処理を行うコード});

リクエストURL: リクエストを送信する先のURLを文字列で渡します
戻り値       : fetch関数を実行すると、response(Responseオブジェクト)がPromiseにラップされた値で返されます
data         : リクエスト送信時の設定をオブジェクトにして渡します
response     : サーバーから返された情報を保持するResponseオブジェクト。ここからデータを使用できるようにアクセスする

fetchメソッドの第二引数には、さまざまな設定をコントロールするためにdataオブジェクトを指定することができます。リクエストの送信時に細かい設定を行うことができます。

  • method:HTTPメソッドを文字列で設定する
  • headers:リクエストヘッダー変更する時に設定する(オブジェクト形式)
  • body:送信するデータ

戻り値のResponseオブジェクトはレスポンスの本文を持っています
様々なプロパティやメソッドがあるため、これを使用してデータのやり取りを行います

まずは、リクエストが成功したのか失敗したのかステータスを確認することが必要です。
例えば、Response.ok、Response.statusなどのプロパティを使用してアクセスに成功したかどうかを調べることができます

よく見かける使用法ではJSON形式でデータを受け取たりしますが、この他にも以下のように様々なデータ形式での受取りが可能です

  • blob() :動画などのバイナリデータ
  • json() :JSON文字列をオブジェクトにして取得
  • text() :レスポンスの文字列をUSVStringオブジェクトとして取得

簡単な例文を書いてみます
JavaScriptを実行するHTMLファイルと同じディレクトリにsample.jsonというファイル名で
ファイルを作成し内容は書きをコピペしてください

sample.json

[
  {"key": "apple", "value": "アップル"},
  {"key": "orange", "value": "オレンジ"},
  {"key": "melon", "value": "メロン"}
]
fetch("sample.json")
  .then((response) => response.json())
  .then((data) => {
    for (const { key, value } of data) {
      console.log(key + ":" + value);
    }
  });

async function myFetch() {
  const response = await fetch("sample.json");
  const data = await response.json(); // jsonメソッドもPromiseを返す
  for (const { key, value } of data) {
    console.log(key + ":" + value);
  }
}
myFetch();

このコードを実行するためには必ずサーバーが起動している必要があります
VScodeであるならばLive Serverを起動すればOKです

長い記事を読んで頂きありがとうございました。
あまり使ったことがないものも多いため、ふと内容を思い返したい時には、一読しここに載せたサンプルコードを少し変更するなどして確実に自分のものにしていきたいと思います

コメントをお待ちしております

お気軽にコメントをどうぞ。

CAPTCHA