JavaScript Promiseとは何かを理解する(初級)

JavaScript

JavaScriptにおける非同期処理の説明はこちらを参照

非同期処理において、エラーファーストコールバックは良い方法が無かった頃の「お作法」程度。

Promiseという「オブジェクト」を使用して統一化された非同期関数を処理することを知る。
今回の参考書籍はこちら。


Promiseとは

私の理解はこれ。

  • 非同期処理と、処理結果のハンドリング、および例外処理をうまくプログラムで表現できるようにしてくれる簡易オブジェクト

オブジェクト、クラス、インスタンス

Promiseという「オブジェクト」を使用した非同期処理とのことだが、Javaを知っている私は、「オブジェクト」ではなく「クラス」では?という疑問を持った。だが言語を勉強する上で、とにかく最初は「これはこういうもの」という素直な受け入れの姿勢は重要だ。郷に入っては郷に従えという心境でJavaScriptに望む。

オブジェクト

JavaScriptではこの言葉が使われまくっていて、Java使いの私からするとすごく違和感があるが、一旦受け入れる事とする。

オブジェクトとは、よく表現や説明で「モノである」とか言われ、例えとして車をオブジェクトに例えて表現される。

私が理解できる表現としては、以下の2つで表現できる。

1つ目は、変数、関数などを定義できる「入れ物」である。

2つ目は、さまざまな定義の枕詞やスコープを定義する「名前」である。オブジェクト「pc」に定義した変数もしくは関数「keyboard」をプログラム上で特定するには「pc.keyboard」で表現できる。

クラス

「クラス」とは、プログラムのテンプレート見たいなもの。ものすごく簡単に言うと、プログラムで使用するデータ領域とそのプログラムを定義する。

JavaScriptには明確に「class」というキーワードがある。これは「ES6」というJavaScriptのedition以降から定義されているようだが、JavaやC#といった言語のいわゆるクラスとは異なるようで、ES6 Edition以前よりprototypeで代用してきたクラスライクなプログラミング手法を、便宜上簡易的に利用できるようにしたものらしい。

JavaやC#を知っているエンジニアからすると細かい仕様が異なるが、概念的にはまさしく「クラス」だ。

※これだけの説明だと、「オブジェクト」と「クラス」って何が違うのかという疑問が湧いてくるが、それは横に置いて先に進むことにする。私の中では今のところ「オブジェクト」という言葉が出てきたら「クラス」という言葉に脳内変換しながら勉強している。

インスタンス

「インスタンス」は、ものすごく簡単に言うと、クラスに定義されたプログラム(Java的には「メソッド」)を実行できるようにするために、プログラムをメモリ上にロードし、クラスに定義されたデータ領域に実際に値を格納できるようにメモリを割り当てて実行可能な状態にしたもの。

インスタンスはクラスから複数生成することができる。この時、クラスに定義されたメソッドは基本的に一度メモリにロードされると再利用される。同じプログラムは何個もいらないからね。データ格納領域はインスタンスごとに新たにメモリが割り当てられる。流石にデータは個別のインスタンスでそれぞれ管理される。

そして、個々のインスタンスがどこのメモリ領域に格納されているかを保持しているのが変数。

同じインスタンスでも、「関数オブジェクト」というものがある。これまた「インスタンス」なのか「オブジェクト」なのか。人によっては「扱いが違う」だの明確に分けたがる人がいるが、言い方が違うだけで基本的には同じ「こと」を指しており、単に「実行できるようにするためにメモリを割り当てたりプログラムをメモリ上にロードする」という事を指している言葉だと理解している。インスタンス化する対象が「クラス」なのか「関数」なのか「メソッド」なのか「データ」なのか、これが違うだけと考えた方が私は理解しやすい。

Promiseインスタンスの生成方法

new演算子でインスタンスを生成する。
※こちらは「インスタンス」。Javaと言い方が一致するからわかりやすい。

const flg = true;

const executor = (resolve, reject) => {
    if (flg) {
        // 成功時:resolve
        resolve({ result: "Resolved"});
    } else {
        // 失敗時:reject
        reject(new Error("Rejected."));
    }
}
const promise = new Promise(executor);
const onResolved = (value) => {
    console.log(value);
}
const onRejected = (error) => {
    console.log(error);
}
promise.then(onResolved, onRejected);

executor関数

上記プログラムでは「executor」という関数が定義されている。

executorとは、本来実装したかった処理本体の事。これをJavaScript内で非同期的にうまくコントロールしたいからPromiseを活用する。

  • Promise内で実行する処理を定義した関数(本来やりたかった処理を記述する)
  • 引数で成功時の関数と失敗時の関数を指定する
  • executor処理成功時、成功時の関数(resolve)をコールするように関数を定義する
  • executor処理エラー時、失敗時の関数(reject)をコールするように関数を定義する

Promiseの処理の流れ

  • インスタンス生成時にexecutor関数を実行(関数を指定しないとTypeError例外となるため必須)
  • executor関数の実行結果を保持 (Fulfilled (成功) or Rejected (失敗))
  • Promise.then()にて関数が登録された時、Promiseの状態がfulfilledなら登録された関数を実行
  • Promise.catch()にて関数が登録された時、Promiseの状態がrejectedなら登録された関数を実行

ここで重要なことは、executorはPromiseインスタンス生成直後の1回しか実行されないということ。

Promiseインスタンスの状態管理

Promiseインスタンスは状態管理されており、状態により挙動を制御している。

  • インスタンスが生成された直後からexecutorの処理が完了するまでは”Pending”
  • executor実行後、executorが正しく完了した場合は”Fulfilled”
  • executor実行後、executorでエラー終了したり例外が発生した場合は”Rejected”

これらの状態は外から知る事ができない内部ステータスとして管理されているようで、確認する手段はないとのこと。

前述した通り、executorはPromiseインスタンス生成時に1回しか実行されない。よって、Promiseインスタンスの状態は再びPendingになったり、FulfilledからRejectedに変わったり、RejectedからFulfilledに変わるということはない。

Promise.then(fulfilledFunction, rejectedFunction)

  • Promiseの状態がfulfilled(成功)およびrejected(失敗)の時に実行するコールバック関数を指定する

Promise.then()で指定したコールバック関数は、Promiseインスタンスの状態に応じて非同期的に呼び出される。引数にコールバック関数を指定いない場合は、何も実行されない。

Promise.catch(rejectedFunction)

  • Promiseの状態がrejected(失敗時)に実行するコールバック関数を指定する

こちらはrejected時のコールバック関数のみ指定できるという違いだけで、やっていることはPromise.then()と同じ。

thenやcatchの実行とexecutorの実行の関係

executorはPromiseインスタンス生成時に1度だけ実行され、それ以降は実行されない。(同じ処理をしたかったらもう一度Promiseインスタンスを生成すれば良い)

それに対して、Promise.then()やPromise.catch()は何度も実行できる。ここでの留意事項は、Promise.then()やPromise.catch()は何度も実行できるが、Promiseインスタンスの状態は変わらないので、常に同じ結果になると言うこと。

例えば一度fulfilled(正常)になったPromiseインスタンスに対してPromise.then()やPromise.catch()を何度実行しても、状態は変わらないので常にPromise.then()で指定したコールバック関数が実行されるということ。先ほどPromiseインスタンスの内部状態は外から参照できないと書いた。実際にはpending(実行中)かどうかは分からないが、一度処理が終わってしまえばPromise.then()やPromise.catch()を使えば確認することができるということだ。

もう一つ重要なことは、Promiseインスタンスに対してPromise.then()やPromise.catch()を何度実行してもどちらのコールバック関数が実行されるかというのは変わらないが、Promise.then()やPromise.catch()に指定するコールバック関数は、実行毎に別のコールバック関数を指定しても良いということ。

executorで発生した例外の扱い

もしexecutorで例外が発生した場合、プロセス全体に影響するような例外スローは発生せず、Promise側で例外の状態を保持しつつ”Rejected”として扱われる。

これはとっても重要なことで、executorで明示的にエラー処理を行なっていないとしても、例外が発生する可能性が少しでもある場合、Rejectedという状態は無視してはいけないということ。

ここまでのPromiseの要約

ここまでで、かなりPromiseが何者かが見えてきた。

Promiseとexecutorの関係

  • Promiseインスタンス生成時にexecutor関数を指定する
  • executorは、Promiseインスタンス生成時に実行され、処理結果を示すステータスをPromiseインスタンスに保持する
  • executorは、executorの第一引数には正常終了時のコールバック関数、第二引数には異常終了時のコールバック関数が指定されるように定義する
  • executorで正常終了時のコールバック関数を実行すると、Promiseインスタンスのステータスとして”Fulfilled”となる(Promiseインスタンスのステータスが確定する)
  • executorで異常終了時のコールバック関数を実行すると、Promiseインスタンスのステータスとして”Rejected”となる(Promiseインスタンスのステータスが確定する)
  • executorの実行中に例外が発生した場合、Promiseインスタンスのステータスは常に”Rejected”となる(Promiseインスタンスのステータスが確定する)

Promiseインスタンス生成後にexecutorの実行結果を知る方法

  • Promiseインスタンスのステータスが”Fulfilled”の時にPromise.then()を実行すると、Promise.then()の引数に指定したコールバック関数が非同期に実行される
  • Proimseインスタンスのステータスが”Rejected”の時にPromise.then()を実行すると、何もしない
  • Promiseインスタンスのステータスが”Fulfilled”の時にPromise.catch()を実行すると、何もしない
  • Promiseインスタンスのステータスが”Rejected”の時にPromise.catch()を実行すると、Promise.catch()の引数に指定したコールバック関数が非同期に実行される
  • Promise.then()やPromise.catch()は必要に応じて何度でも実行できる(どちらのコールバック関数が実行されるかどうかはProimseインスタンスのステータスによる)
  • Promise.then()やPromise.catch()の実行時に指定するコールバック関数は、実行の都度別のコールバック関数を指定できる

ようやくPromiseの正体がわかった。
次回以降でPromiseの活用方法として「Promiseチェーン」という概念を共有する。

今回の参考書籍はこちら。


コメント

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