Promise

Promise

Promiseクラスは、多くのモダンなJavaScriptエンジンに存在しており、簡単にpolyfillすることができます。Promiseを使いたい理由は、非同期/コールバック的なスタイルのコードに対して、同期処理の書き方でエラーを取り扱うことができるからです。

コールバックを用いたスタイルのコード

Promiseを完全に理解するため、信頼性の高い非同期処理をコールバックだけで構築する難しさをサンプルコードで示します。ファイルからJSONをロードする処理の非同期バージョンを作成するケースを考えてみましょう。これの同期処理バージョンは非常にシンプルです:

import fs = require('fs');

function loadJSONSync(filename: string) {
    return JSON.parse(fs.readFileSync(filename));
}

// 正しいjsonファイル
console.log(loadJSONSync('good.json'));

// 存在しないファイル: fs.readFileSync が失敗する
try {
    console.log(loadJSONSync('absent.json'));
}
catch (err) {
    console.log('absent.json error', err.message);
}

// 正しくないjsonファイル 例: ファイルは存在するが、JSON.parseが失敗する
try {
    console.log(loadJSONSync('invalid.json'));
}
catch (err) {
    console.log('invalid.json error', err.message);
}

このシンプルなloadJSONSyncには有効な戻り値、ファイルシステムエラー、JSON.parseエラーの3種類の動作があります。私たちは、他の言語で同期処理を行う際に慣れていたように、単純なtry/catchでエラーを処理します。さて、このような関数について、良い関数を非同期バージョンで作ってみましょう。最初の素朴な試みとして作成した関数です。些細なエラーチェックのロジックを追加しています。:

十分にシンプルです。コールバックを引数に受け取り、ファイルシステムのエラーをコールバックに渡します。ファイルシステムのエラーがなければ、JSON.parseの結果を返します。コールバックに基づいて非同期関数を利用するときに注意すべき点は次のとおりです。

  1. 決してコールバックを2回呼ばないでください。

  2. 決して Error をthrowしないでください。

しかしながら、この単純な関数は2つ目の点に対応できません。実際にJSON.parseに間違ったJSONが渡されると、Error がthrowされ、コールバックが呼び出されず、アプリケーションがクラッシュします。これを以下の例で示します:

これを修正するための素朴な試みは、次の例に示すようにJSON.parseをtry catchで囲むことです。

しかし、このコードには些細なバグがあります。もしコールバック(cb)が呼び出され、JSON.parseが呼び出されずに、エラーをthrowした場合、try/catchで囲んでいるため、catchが実行され、コールバックを再度呼び出してしまいます。つまり、コールバックが二度呼び出されてしまいます!これを以下の例で示します:

これは、loadJSON関数が間違ってtryブロックでコールバックを囲んでいるためです。ここで覚えておくべき簡単な教訓があります。

シンプルな教訓:コールバックをコールするとき以外のすべての同期処理コードをtry catchで囲むこと。

このシンプルな教訓に基づいて、私達は完全に機能する非同期バージョンのloadJSONを作成できます:

確かに何回か行えば、そう難しいことではありませんが、単に、良いエラー処理を書くためだけに、このような多くの定型的なコードが必要です。では、Promiseを使ってJavaScriptの非同期処理に取り組むための、より良い方法を見てみましょう。

Promiseを作る

Promiseの状態は、pending(保留中)またはfulfilled(履行済み)またはrejected(拒絶済み)のいずれかになります。

Promiseの宣言と運命

Promiseの作り方を見てみましょう。Promise(Promiseのコンストラクタ)に対してnewを呼び出すだけです。Promiseのコンストラクタには、Promiseの状態を決めるためのresolve関数とreject関数が渡されます。

Promiseの結果を監視(subscribing)する

Promiseの結果は、.then(resolveが実行された場合)または.catch(rejectが実行された場合)を使用して監視(Subscribe)できます。

TIP:Promiseのショートカット

  • すでにresolveされているPromiseをクイックに作成する:Promise.resolve(result)

  • 既にrejectされているPromiseをクイックに作成する: Promise.reject(error)

Promiseのチェーン

PromiseのチェーンはPromiseを使う最大のメリットです。一度Promiseを取得すれば、その時点から、then関数を使ってPromiseのチェーンを作れます。

  • もしチェーン内の関数からPromiseを返すと、そのPromiseがresolveされた時に1回だけ.thenが呼び出されます:

  • チェーンの前の部分のエラー処理を単一のcatchに集約することができます:

  • catchは実のところ新しいPromiseを返します(要するに新しいPromiseのチェーンを作成します):

  • then(またはcatch)で同期エラーがスローされると、返されたPromiseが失敗します:

  • エラーが発生すると関係している(後方で最も近い)catchだけがコールされます(同時にcatchが新しいPromiseのチェーンを作ります)。

  • catchはチェーンの前部分でエラーが発生した場合にのみコールされます:

Promiseチェーンに関する事実:

  • エラーが起きた場合、後続のcatchにジャンプします(そして途中のthenはスキップします)

  • 同期処理のエラーについても同様に、最も近い後続のcatchで捕捉されます

Promiseは、単なるコールバックに比べて優れたエラー処理を可能にする非同期プログラミングのパラダイムを我々に提供してくれます。さらに下記に詳しく記載します。

TypeScriptとPromise

TypeScriptが素晴らしい点は、それがPromiseチェーンを通じて流れる値を理解してくれることです。

もちろん、Promiseを返す可能性のある関数呼び出しも理解してくれます。

コールバック関数からPromiseを返すように変更する

関数呼び出しをPromiseに包んで、下記のようにしてみましょう。

  • エラーが発生した場合は rejectを呼出す

  • すべてうまく行った場合はresolveを呼び出す

例えばfs.readFileをPromiseで囲んでみましょう:

JSONの例を見直す

次に、loadJSONの例を見直して、Promiseを使う非同期バージョンを書いてみましょう。やるべきことは、Promiseとしてファイル内容を読み、JSONとしてパースする、それだけです。これを以下の例で示します:

使い方(このセクションの始めに紹介した同期処理のコードとの違いに注目してください🌹):

この関数がよりシンプルになった理由は、"loadFile(async)+JSON.parse(sync)=>catch"の連結をPromiseチェーンによって行ったためです。また、コールバックは我々ではなくPromiseチェーンによって呼び出されるので、コールバックをtry/catchで囲んでしまう誤りが起きる可能性はありませんでした。

並列制御フロー(Parallel control flow)

私たちは、Promiseを使って非同期タスクの順次処理(シーケンシャル処理)を行うことがいかに簡単かを見てきました。単にthenの呼び出しを連結するだけなのです。

しかし、複数の非同期処理を実行し、すべてのタスクが終わったタイミングで何らかの処理を行いたいケースがあるかもしれません。Promiseは静的なPromise.all関数を提供します。この関数は、n個のPromiseがすべて完了するまで待つことができます。n個のPromiseの配列を渡すと、n個の解決された値の配列を返します。以下では、Promiseチェーンの例と合わせて並列で処理する例を示します。

ある場合には、複数の非同期タスクを実行するが、これらのタスクの内1つだけが完了すれば良いケースもあるでしょう。Promiseは、このユースケースに対してPromise.raceというStatic関数を提供しています:

コールバック関数をPromiseに変換する

これを行うために信頼性が高い方法は、自分でコードを書くことです。例えばsetTimeoutをPromiseを使ったdelay関数に変換するのは非常に簡単です:

NodeJSにはこれを行う便利で使いやすい関数があることを知っておいてください。これは コールバックスタイルの関数 => Promiseを返す関数 に変える魔法をかけてくれます。

最終更新

役に立ちましたか?