こんにちはWebエンジニアのmasakichiです。
JavaScriptの非同期を理解するにはMDN Web Docsを読むべき。翻訳しといたよ 3つ目の記事です。
全部で5記事あります。
- Introducing asynchronous JavaScript
- Cooperative asynchronous JavaScript: Timeouts and intervals
- Graceful asynchronous programming with Promises ←いまここ
- Making asynchronous programming easier with async and await
- Choosing the right approach
翻訳箇所について
こちらのページの日本語未翻訳記事です。
なぜ翻訳したか
JavaScriptの非同期処理。特にPromiseというやつが、ぼんやりわかっているけど完全には理解していない状態がずっと続いていました。
そんな中、MDN Web Docsの解説がすごくわかりやすく一気に理解できました。
しかし、これらの記事は日本語に翻訳されていないという問題が。。。
ぜひ非同期処理で悩める同志にも読んでほしい。という想いで翻訳作業をしてみました。
留意点
筆者は英語がそこまで得意というわけではありません。DeepLの力を借りて、翻訳していますので、日本語訳が不自然なところや一部、情報を省略・意訳しています。あらかじめご了承ください。
ライセンスは下記です。
This Article by Mozilla Contributors is licensed under CC-BY-SA 2.5.
Promiseを用いた優美な非同期プログラミング
PromiseはJavaScript言語の比較的新しい機能で、前のアクションが完了するまで次のアクションを延期したり、その失敗に対して応答したりすることができるものです。これは一連の非同期処理を正しく動作するよう設定するのに便利です。この記事では、Promiseがどのように機能するか、Web APIとしてどのように使うのか、そしてその記述方法について説明します。
前提条件
基本的なコンピュータリテラシー / JavaScriptの基本をそれなりに理解していること。
目的
Promiseについて、使い方も含めて理解すること。
Promiseとは何か?
講座の第1回でPromiseについて簡単に見てきましたが、ここではもっと深く見ていきましょう。
基本的に、Promiseは操作の中間状態を表すオブジェクトで、事実上、将来のある時点で何らかの結果が返されることをPromiseするものです。いつオペレーションが完了し結果が返されるかは保証されていませんが、結果が利用可能になったとき、あるいはPromiseが失敗したときに、成功した結果に対して何か別の処理を行うため、あるいは失敗したケースを優雅に処理するために、提供したコードが実行されるという保証はあります。
一般的に、非同期処理が結果を返すまでにかかる時間にはあまり興味がなく(もちろん、あまりにも時間がかかりすぎる場合は別です!)、結果が返されたときにいつでも対応できることに興味があるのではないでしょうか。そしてもちろん、残りのコードが実行されるのをブロックしないのはいいことです。
Promiseの中で最も一般的なのは、Promiseを返すWebAPIです。仮想的なビデオチャットアプリケーションを考えてみましょう。このアプリケーションには、ユーザーの友人のリストが表示されたウィンドウがあり、ユーザーの横にあるボタンをクリックすると、そのユーザーとのビデオ通話が開始されます。
このボタンのハンドラは、ユーザーのカメラとマイクにアクセスするために、getUserMedia()
を呼び出します。getUserMedia()
は、ユーザーがこれらのデバイスを使用する許可を持っていることを確認し、どのマイクを使用するか、どのカメラを使用するか(または音声のみの通話にするか、その他のオプション)をユーザーに尋ねる必要があるため、これらの決定だけでなく、カメラとマイクが作動するまでプログラムをブロックしてしまいます。また、ユーザーはこれらの許可要求にすぐには応じないかもしれません。これは、潜在的に長い時間がかかる可能性があります。
getUserMedia()
の呼び出しはブラウザのメインスレッドから行われるので、getUserMedia()
が戻るまでブラウザ全体がブロックされることになるのは、許容できる選択肢ではありません。Promiseがなければ、ユーザーがカメラとマイクについてどうするか決定するまで、ブラウザ内のすべてが使用不能になります。そこで、ユーザーを待ち、選択したデバイスを有効にし、選択したソースから作成したストリームのMediaStreamを直接返す代わりに、getUserMedia()
はPromiseを返し、MediaStreamが利用可能になった時点で解決するようにしています。
ビデオチャットアプリケーションが使用するコードは、次のようなものでしょう。
function handleCallButton(evt) {
setStatusMessage("Calling...");
navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then(chatStream => {
selfViewElem.srcObject = chatStream;
chatStream.getTracks().forEach(track => myPeerConnection.addTrack(track, chatStream));
setStatusMessage("Connected");
}).catch(err => {
setStatusMessage("Failed to connect");
});
}
この関数は、まずsetStatusMessage()という関数を使って、ステータス表示を「Calling...」というメッセージで更新し、通話を試みていることを示します。次に getUserMedia() を呼び出して、ビデオとオーディオの両方のトラックを持つストリームを要求し、それが取得されると、カメラから来るストリームを「セルフビュー」として表示するようにビデオ要素をセットアップし、ストリームのトラックをそれぞれ取得して、他のユーザーとの接続を表す WebRTC RTCであるPeerConnection に追加しています。その後、ステータス表示を更新し、「接続済み」と表示します。
getUserMedia()が失敗すると、catchブロックが実行されます。これは setStatusMessage() を使用して、エラーが発生したことを示すためにステータスボックスを更新します。
ここで重要なのは、カメラストリームがまだ取得されていなくても、getUserMedia()の呼び出しがほぼ即座に返ってくるということです。handleCallButton()関数が、それを呼び出したコードにすでに戻っていたとしても、getUserMedia()の動作が終了すると、あなたが提供したハンドラが呼び出されるのです。アプリは、ストリーミングが開始されたと仮定しない限り、そのまま実行し続けることができます。
コールバックの問題点
Promiseがなぜ良いものなのかを十分に理解するためには、古いスタイルのコールバックを思い返し、なぜそれが問題なのかを理解することが役立ちます。
例として、ピザの注文の話をしましょう。注文を成功させるためには、いくつかのステップを踏まなければなりませんが、そのステップを順番に実行しなかったり、順番に実行しても、前のステップが完全に終了する前に実行しては意味がなくなります。
- 好きなトッピングを選びます。優柔不断な人は時間がかかるかもしれませんし、どうしても決まらなかったり、カレーにした場合は失敗する可能性があります。
- そして、注文をします。ピザを返すのに時間がかかったり、ピザを焼くのに必要な材料が店になかったりすると失敗することがあります。
- ピザを受け取り、食べます。このとき、財布を忘れて代金を支払えなかったりすると、失敗するかもしれません。
古いスタイルのコールバックでは、上記の機能を擬似的にコード化すると次のようになります。
chooseToppings(function(toppings) {
placeOrder(toppings, function(order) {
collectOrder(order, function(pizza) {
eatPizza(pizza);
}, failureCallback);
}, failureCallback);
}, failureCallback);
ごちゃごちゃして読みにくいですね(「コールバック地獄」と言われます)。また、ステップが失敗した時の処理であるfailureCallback()
を複数回(ネストした関数ごとに1回)呼び出す必要があります。さらに他の問題もあるでしょう。
Promiseによる改善策
Promiseは、上記のような状況をより簡単に記述し、解析し、実行することができます。もし上記の疑似コードを非同期Promiseを代わりに使って表現すると、以下のようなものになります。
chooseToppings()
.then(function(toppings) {
return placeOrder(toppings);
})
.then(function(order) {
return collectOrder(order);
})
.then(function(pizza) {
eatPizza(pizza);
})
.catch(failureCallback);
何が起こっているのかがわかりやすく、すべてのエラーを処理するための .catch() ブロックがひとつで済み、メインスレッドをブロックせず(つまり、ピザが手に入れるまでの間もビデオゲームを続けられる)、各処理は前の処理が完了するのを待ってから実行することが保証されているのです。この方法で複数の非同期アクションを連鎖的に実行することができます。なぜなら、それぞれの .then() ブロックは新しいPromiseを返し、.then() ブロックの実行が終了したときに解決されるからです。賢いでしょう?
アロー関数を使えば、さらにコードをシンプルにすることができます。
chooseToppings()
.then(toppings =>
placeOrder(toppings)
)
.then(order =>
collectOrder(order)
)
.then(pizza =>
eatPizza(pizza)
)
.catch(failureCallback);
また、こうも書けます
chooseToppings()
.then(toppings => placeOrder(toppings))
.then(order => collectOrder(order))
.then(pizza => eatPizza(pizza))
.catch(failureCallback);
これは、アロー関数の場合、() => x が () => { return x; } の有効な略記法であるため、うまくいきます。
このように、関数は引数を直接渡すだけなので、余計な関数のレイヤーは必要ないのです。
chooseToppings().then(placeOrder).then(collectOrder).then(eatPizza).catch(failureCallback);
しかし、この構文は読みやすくありませんし、ここで紹介したものよりも複雑なブロックの場合は使えないかもしれません。
注:async/await構文でさらに改良することができますので、次回の記事で掘り下げます。
基本的に、Promiseはイベントリスナーとよく似ていますが、いくつかの違いがあります。
- Promiseは一度だけ成功または失敗することができます。2回成功したり失敗したりすることはできませんし、処理が完了した後に成功から失敗に切り替わることもありません。
- Promiseが成功または失敗した後、成功/失敗のコールバックを追加すると、イベントが先に発生した場合でも、正しいコールバックが呼び出されます。
基本的なPromiseの構文 : 実例
最近の Web API の多くは、潜在的に長大なタスクを実行する関数にPromiseを使用しているため、Promiseを理解することは重要です。最新のWeb技術を使うには、Promiseを使う必要があります。この章の後半で、あなた自身のPromiseを書く方法を見ていきますが、今は、あなたがWeb APIで遭遇するいくつかの簡単な例を見ていきます。
最初の例では、fetch()メソッドでウェブから画像を取得し、Response.blob()メソッドで取得したレスポンスの生のボディコンテンツをBlobオブジェクトに変換し、<img>
要素内にそのBlobを表示することにします。これはシリーズの最初の記事で見た例と非常によく似ていますが、あなた自身のPromiseベースのコードを構築するために、少し違った方法で行います。
注:以下の例は、ファイルから直接実行しただけでは(つまり、file://のURL経由で)動作しません。ローカルのテストサーバーで実行するか、GlitchやGitHubページなどのオンラインソリューションを使用する必要があります。
-
まず、簡単なHTMLテンプレートと、取得するサンプル画像ファイルをダウンロードします。
-
HTMLの
<body>
の一番下に<script>
要素を追加します。
-
<script>
要素の中に、次の行を追加してください。
let promise = fetch('coffee.jpg');
これは fetch() メソッドを呼び出し、パラメータとしてネットワークから取得する画像の URL を渡します。これはオプションの第2パラメータとしてoptionsオブジェクトを受け取ることもできますが、ここでは最も単純なバージョンを使用します。fetch()が返すpromiseオブジェクトをpromiseという変数に格納しています。前述したように、このオブジェクトは最初は成功でも失敗でもない中間状態を表します。この状態のPromiseの正式な用語はpendingです。
-
Primiseの操作が成功したとき(この場合はResponseが返されたとき)に対応するには、Promiseオブジェクトの.then()メソッドを呼び出します。.then()ブロック内のコールバックは、Promiseの呼び出しが正常に完了し、Responseオブジェクトを返したときだけ実行されます。返されたResponseオブジェクトはパラメータとして渡されます。
注意: .then()ブロックの動作は、AddEventListener()を使ってオブジェクトにイベントリスナーを追加するときと似ています。イベントが発生するまで(Promiseが実行されるまで)実行されません。最も顕著な違いは、イベントリスナーが複数回呼び出される可能性があるのに対し、.then()は使用されるたびに一度だけ実行されることです。
このレスポンスに対して直ちにblob()メソッドを実行し、レスポンスのボディが完全にダウンロードされたことを確認し、利用可能になったら、それを何かできるようにBlobオブジェクトに変換します。この結果は、次のように返されます。response => response.blob()
ショートハンドを使わずに書くとこうなります。function(response) { return response.blob(); }
残念ながら、もう少し多くのことを行う必要があります。FetchのPromiseは404や500のエラーで失敗しません - ネットワーク障害のような壊滅的な何かでだけ失敗します。しかし、404や500のエラーは成功しますが、response.okプロパティがfalseに設定されています。たとえば404でエラーを発生させるには、response.okの値をチェックし、falseの場合はエラーをスローし、trueの場合にのみblobを返す必要があります。これは次のように行うことができます。JavaScriptの最初の行の下に次の行を追加します。let promise2 = promise.then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { return response.blob(); } });
-
.then()は、毎回新しいPromiseを作成します。blob()メソッドもPromiseを返すので、2つ目のPromiseの.then()メソッドを呼び出せば、blob()メソッドが返すBlobオブジェクトをfullfilled時に処理することができます。Blobに対して単一のメソッドを実行して結果を返すだけでなく、もう少し複雑なことをしたいので、今回は関数本体を中括弧で囲む必要があります(そうしないとエラーがスローされます)。コードの末尾に以下を追加します。
let promise3 = promise2.then(myBlob => {) });
-
次に、.then()コールバックの本体を埋めてみましょう。中括弧の中に次の行を追加します。
let objectURL = URL.createObjectURL(myBlob); let image = document.createElement('img'); image.src = objectURL; document.body.appendChild(image);
ここでは、URL.createObjectURL()メソッドを実行し、2番目のPromiseのfullfilledで返されたBlobをパラメータとして渡しています。これは、オブジェクトを指すURLを返します。次に、
<img>
要素を作成し、その src 属性をオブジェクトの URL と同じに設定して DOM に追加すると、画像がページに表示されます!
作成したHTMLファイルを保存して、ブラウザで読み込むと、期待通りにページ内に画像が表示されていることが確認できます。お疲れ様でした。
失敗への対応
現在、Promiseの1つが失敗した(Promiseで言えばrejected)場合のエラーを明示的に処理するものは何もありません。前のPromiseから .catch() メソッドを実行することで、エラー処理を追加することができます。下記を追加してください。
let errorCase = promise3.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
これを実際に見るには、画像のURLを間違えて、ページを再読み込みしてみてください。ブラウザのデベロッパーツールのコンソールにエラーが表示されます。
これは、わざわざ.catch()ブロックを含めるほどでもありませんが、考えてみて下さい。実際のアプリケーションでは、.catch()ブロックは画像の取得を再試行したり、デフォルトの画像を表示したり、別の画像URLを提供するようユーザーに促したり、さまざまなことが可能です。
ブロックを連鎖させる
今までのコードは非常に長ったらしい書き出しをしていますが、何が起こっているのかを明確に理解してもらうために、あえてそうしています。この記事の前半で示したように、.then()ブロック(と.catch()ブロック)を連鎖させることが可能です。次のように書くこともできます。
fetch('coffee.jpg')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
return response.blob();
}
})
.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});
Promiseが返す値は、次の .then() ブロックのコールバック関数に渡されるパラメータになることに留意してください。
注:Promiseにおける .then()/.catch() ブロックは基本的に同期コードにおける try...catch ブロックの非同期版に相当します.同期的なtry...catchは非同期的なコードでは動作しないことを心に留めておいてください。
Promise用語のおさらい
上記のセクションでカバーすべきことがたくさんありましたので、簡単に振り返ります。記憶をリフレッシュするために使える短いガイドを提供しましょう。また、これらの概念が定着するように、上記のセクションをもう何度か読み返してみてください。
- Promiseが作成されたとき、それは成功でも失敗でもない状態です。保留状態であるという。
- Promiseが戻ってきたとき、それは解決されたと言います。
- 解決に成功したPromiseは,fulfilledと呼ばれます.Promiseは値を返し,その値は .then() ブロックをPromiseチェーンの末尾に連結することでアクセスすることができます..then()ブロック内のコールバック関数には,Promiseの戻り値が格納されます.
- 解決に失敗したPromiseはrejectedと言われています。これは、Promiseが拒否された理由を示すエラーメッセージを返します。この理由には,Promise・チェーンの最後に .catch() ブロックを連結してアクセスすることができます.
複数のPromiseのfullfilledに応答してコードを実行させる
上記の例では、Promiseを使用するための基本をいくつか示しました。次に、より高度な機能を見てみましょう。まず最初に、ある処理を次々と発生させるチェーニングは問題ありませんが、もしあるコードを実行するときに、たくさんのPromiseがすべてfullfilledした後に実行したいとしたらどうでしょうか?
これは、Promise.all() という独創的な名前の静的メソッドで実現できます。これは入力パラメータとしてPromiseの配列を受け取り、配列内のすべてのPromiseがfullfilledした場合にのみfullfilledする新しいPromiseオブジェクトを返します。このような感じです。
Promise.all([a, b, c]).then(values => {
...
});
それらがすべてfullfilledした場合、連鎖した .then() ブロックのコールバック関数に、それらの結果をすべて含む配列がパラメータとして渡されます。Promise.all()に渡されたPromiseのいずれかがrejectedされた場合、ブロック全体がrejectedされます。
これはとても便利なことです。ページ上のUI機能に動的にコンテンツを投入するために情報を取得することを想像してください。多くの場合、部分的な情報を表示するよりも、すべてのデータを受け取ってから完全なコンテンツを表示する方が理にかなっています。
このことを示すために、別の例を作ってみましょう。
-
テンプレートの新しいコピーをダウンロードし、再び終了
</body>
タグの直前に<script>
要素を配置します。
- ソースファイル(coffee.jpg、tea.jpg、description.txt)をダウンロードするか、ご自由にお使いください。
- このスクリプトでは、まず Promise.all() に渡したいPromiseを返す関数を定義します。これは、単に3つのfetch()処理の完了に応答してPromise.all()ブロックを実行したい場合は簡単でしょう。次のようなことをすればいいのです。
Promiseがfullfilledになると、ハンドラーに渡される値には、完了したfetch()操作ごとに1つずつ、合計3つのResponseオブジェクトが含まれることになります。しかし、このようなことはしたくありません。私たちのコードは、fetch() 操作がいつ完了したかを気にしていません。私たちが欲しいのは、読み込まれたデータなのです。つまり、画像を表す使用可能なBlobと使用可能なテキスト文字列を取得したときに、Promise.all()ブロックを実行したいのです。これを実現する関数を作成します。let a = fetch(url1); let b = fetch(url2); let c = fetch(url3); Promise.all([a, b, c]).then(values => { ... });
<script>
要素内に次のように記述します。
少し複雑に見えるので、順を追って説明しましょう。function fetchAndDecode(url, type) { return fetch(url).then(response => { if(!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } else { if(type === 'blob') { return response.blob(); } else if(type === 'text') { return response.text(); } } }) .catch(e => { console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message); }); }
- まず最初に、関数を定義し、URL と取得するリソースの種類を表す文字列を渡します。
2. 関数本体の内部は、最初の例で見たものと同様の構造になっています。指定されたURLのリソースを取得するためにfetch()関数を呼び出し、それを別のPromiseに連結して、デコードした(または「読み取り」)レスポンスボディを返します。これは、前の例では常にblob()メソッドでした。
3. しかし、ここでは2つの点が異なっています。 - まず、型値が何であるかによって、2番目に返すPromiseが異なります。.then()コールバック関数の内部には、デコードする必要があるファイルのタイプに応じて異なるPromiseを返す、単純なif ... else if文が含まれています(この場合、blobまたはtextの選択肢がありますが、他のタイプを扱うためにこれを拡張することは容易です)。 - 次に、fetch()呼び出しの前にreturnキーワードを追加しています。この効果は、チェーン全体を実行し、最終結果(つまり、blob()またはtext()が返すPromise)を今定義した関数の戻り値として実行することです。事実上、return文は結果をチェーンの上部に渡しています。
4. ブロックの最後に、.catch()呼び出しにチェーンして、.all()に配列で渡されたPromiseで発生する可能性のあるあらゆるエラーケースを処理します。もしどれかのPromiseがrejectedされたら、.catch()ブロックはどれが問題だったかを知らせます。.all()ブロック(下記参照)はまだfullfilledされますが、問題があったリソースを表示することはありません。一度 .catch() ブロックでPromiseを処理すると、結果のPromiseは解決されたとみなされますが、値は未定義であることを覚えておいてください; このため、この場合 .all() ブロックは常にfullfilledされます。もし、.all()を拒否したい場合は、代わりに.catch()ブロックを.all()の最後に連結する必要があります。
関数本体内のコードは非同期でPromiseベースなので、事実上、関数全体がPromiseのように動作します。
- 次に、関数を3回呼び出して、画像とテキストのフェッチとデコードの処理を開始し、返されたPromiseをそれぞれ変数に格納します。前のコードの下に以下を追加してください。
let coffee = fetchAndDecode('coffee.jpg', 'blob'); let tea = fetchAndDecode('tea.jpg', 'blob'); let description = fetchAndDecode('description.txt', 'text');
- 次に、上に格納した3つのPromiseがすべて成功したときだけ、あるコードを実行する Promise.all() ブロックを定義することにします。まず始めに、.then()の呼び出しの中に空のコールバック関数を持つブロックを以下のように追加します。
Promise.all([coffee, tea, description]).then(values => { });
パラメータとしてPromiseを含む配列を受け取っているのがわかると思います。.then()コールバック関数は3つのPromiseが解決したときだけ実行されます; それが起こるとき、それは個々のPromiseからの結果を含む配列に渡されます(すなわち、デコードされた応答ボディ)、[coffee-results, tea-results, description-results] みたいな感じです。
-
最後に、コールバック内に以下を追加します。ここでは、かなり単純な同期コードを使用して、結果を個別の変数に格納し(blobからオブジェクトURLを作成)、画像とテキストをページに表示します。
console.log(values); // Store each value returned from the promises in separate variables; create object URLs from the blobs let objectURL1 = URL.createObjectURL(values[0]); let objectURL2 = URL.createObjectURL(values[1]); let descText = values[2]; // Display the images in <img> elements let image1 = document.createElement('img'); let image2 = document.createElement('img'); image1.src = objectURL1; image2.src = objectURL2; document.body.appendChild(image1); document.body.appendChild(image2); // Display the text in a paragraph let para = document.createElement('p'); para.textContent = descText; document.body.appendChild(para);
-
保存して更新すると、UIコンポーネントがすべて読み込まれているのが見えるはずです。
ここで提供した項目を表示するコードはかなり初歩的なものですが、とりあえず説明用として機能します。
Promiseの実行後に、結果に関わらず最終的なコードを実行する
Promiseが完了した後、それがfullfilledしたか否かに関わらず、最終的なコードブロックを実行したい場合があります。以前は、例えば .then() と .catch() の両方のコールバックで同じコードを記述する必要がありました。
myPromise
.then(response => {
doSomething(response);
runFinalCode();
})
.catch(e => {
returnError(e);
runFinalCode();
});
最近のモダンブラウザでは、.finally()メソッドが利用可能で、通常のPromiseチェーンの末尾に連結することができ、コードの繰り返しを減らし、よりエレガントに物事を行うことができるようになりました。上記のコードは以下のように書くことができます。
myPromise
.then(response => {
doSomething(response);
})
.catch(e => {
returnError(e);
})
.finally(() => {
runFinalCode();
});
実際の例として、promise-finally.htmlのデモをご覧ください(ソースコードもご覧ください)。これは上のセクションで見たPromise.all()のデモと同じように動作しますが、fetchAndDecode()関数の中で、チェーンの終わりにfinally()を連結している点が異なります。
function fetchAndDecode(url, type) {
return fetch(url).then(response => {
if(!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
} else {
if(type === 'blob') {
return response.blob();
} else if(type === 'text') {
return response.text();
}
}
})
.catch(e => {
console.log(`There has been a problem with your fetch operation for resource "${url}": ` + e.message);
})
.finally(() => {
console.log(`fetch attempt for "${url}" finished.`);
});
}
.finally(()では、それぞれのfetchが終了したときに、コンソールに簡単なログを出しています。
独自のカスタムPromiseを構築
ある意味で、あなたはすでに自分自身のPromiseを構築しています。複数のPromiseを .then() ブロックで連結したり、組み合わせて独自の機能を作ったりしているとき、あなたはすでに独自の非同期Promiseベースの関数を作っていることになります。例えば、先ほどの例の fetchAndDecode() 関数を見てみましょう。
異なるPromiseベースのAPIを組み合わせてカスタム機能を作成することは,Promiseを使って物事をカスタムする最も一般的な方法であり,最新のAPIのほとんどが同じ原理をベースにしていることの柔軟性とパワーを示しています。しかし、もう一つの方法があります。
Promise()コンストラクタの使用
Promise()コンストラクタを使用して、独自のPromiseを構築することが可能です。主な用途としては、Promiseに対応していない旧来の非同期APIをベースとしたコードをPromise化する場合です。これは、既存の古いプロジェクトのコードやライブラリ、フレームワークを、最新のPromiseベースのコードと一緒に使う必要がある場合に便利です。
ここでは、setTimeout()呼び出しをPromiseでラップしています。これは2秒後に関数を実行し、(渡されたresolve()呼び出しを使用して)Promiseを解決して「成功!」という文字列を返します。
let timeoutPromise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Success!');
}, 2000);
});
resolve()とreject()は、新しく作成されたPromiseをfulfillまたはrejectするために呼び出される関数です。この場合、Promiseは "Success!"という文字列でfullfilledされます。
つまり、このPromiseを呼び出したら、その末尾に.then()ブロックをチェーンさせれば、「成功!」という文字列が渡されることになるわけです。以下のコードでは、そのメッセージを警告しています。
timeoutPromise
.then((message) => {
alert(message);
})
もしくは、こうも書けます。
timeoutPromise.then(alert);
上記の例では、Promiseは単一の文字列でしか実現できず、reject()条件も指定されていません(確かに、setTimeout()には失敗条件がないので、この単純な例では問題にはなりません)。
カスタムPromiseをrejectする
resolve()と同じように、これは1つの値を取りますが、この場合、それをrejectする理由、つまり.catch()ブロックに渡されるエラーです。
先ほどの例を拡張して、reject() の条件をいくつか設定し、成功時にさまざまなメッセージを渡せるようにしましょう。
前の例をコピーして、既存のtimeoutPromise()の定義を次のように置き換えてください。
function timeoutPromise(message, interval) {
return new Promise((resolve, reject) => {
if (message === '' || typeof message !== 'string') {
reject('Message is empty or not a string');
} else if (interval < 0 || typeof interval !== 'number') {
reject('Interval is negative or not a number');
} else {
setTimeout(() => {
resolve(message);
}, interval);
}
});
}
ここでは、カスタム関数に2つの引数を渡しています。何かをするためのメッセージと、何かをする前に経過させる時間間隔です。関数内では新しいPromiseオブジェクトを返しています。この関数を呼び出すと、使用したいPromiseが返されます。
Promiseコンストラクタの内部では、if ... else構造体の中でいくつかのチェックを行っています。
- まず、メッセージが警告されるにふさわしいかどうかをチェックする。もしそれが空文字列であったり、文字列でない場合は、適切なエラーメッセージを表示してPromiseを拒否します。
- 次に、intervalが適切なインターバル値であるかどうかをチェックする。もしそれが負の値であったり、数値でない場合は、適切なエラーメッセージとともにPromiseを拒否します。
- 最後に,パラメータに問題がなければ,setTimeout()を用いて,指定された時間経過後に,指定されたメッセージとともにPromiseを解決します.
timeoutPromise()関数はPromiseを返すので、.then()や.catch()などを連鎖させてその機能を利用することができます。それでは使ってみましょう。先ほどのtimeoutPromiseの使い方をこの関数に置き換えてみてください。
timeoutPromise('Hello there!', 1000)
.then(message => {
alert(message);
})
.catch(e => {
console.log('Error: ' + e);
});
このまま保存して実行すると、1秒後にメッセージのアラートが表示されます。ここで、例えばメッセージに空文字列を設定したり、間隔に負の数を設定してみると、適切なエラーメッセージとともにPromiseがrejectされるのがわかるはずです!また、解決したメッセージに対して、単にアラートを出すだけでなく、何か別の処理をしてみることもできます。
結論
Promiseは、関数の戻り値や戻り値にかかる時間が分からない場合に、非同期アプリケーションを構築するのに適した方法です。また、同期的なtry...catch文と同じようなエラーハンドリングもサポートしています。
Promiseはすべてのモダンブラウザの最新バージョンで動作します。Promiseのサポートが問題になるのは、Opera MiniとIE11およびそれ以前のバージョンだけです。
この記事では、Promiseのすべての機能には触れず、最も興味深く、便利な機能だけを紹介しました。Promiseについてもっと学び始めると、さらなる機能やテクニックに出会えるでしょう。
最近のWeb APIはほとんどがPromise型なので、Promiseを理解しないと使いこなせません。それらのAPIの中には、WebRTC、Web Audio API、Media Capture and Streams、その他多くのものがあります。Promiseは時代が進むにつれてますます重要になるので、Promiseの使用と理解を学ぶことは、モダンなJavaScriptを学ぶ上で重要なステップとなります。