コールバックの代わりにプロミスを使用すると、読みやすいより簡潔なコードを作成できます。 ただし、それらに精通していない人にとっては、特に明確に見えないかもしれません。 この記事では、Promiseを操作するための基本的なテンプレートを示し、不適切なアプリケーションが引き起こす可能性のある問題についてのストーリーを共有したいと思います。
ここでは矢印関数を使用することに注意してください。 それらに精通していない場合、それらは複雑ではないと言う価値がありますが、この場合、それらの機能に関する資料を読むことをお勧めします。
パターン
このセクションでは、約束、およびそれらを正しく使用する方法について説明し、アプリケーションのいくつかのパターンを示します。
Pro約束の使用
すでにプロミスをサポートしているサードパーティのライブラリを使用する場合、それらの使用は非常に簡単です。 つまり、
then()
と
catch()
2つの関数に注意する必要があります。 たとえば、
getItem()
、
updateItem()
、および
deleteItem()
3つのメソッドを持つAPIがあり、それぞれがpromiseを返します。
Promise.resolve() .then(_ => { return api.getItem(1) }) .then(item => { item.amount++ return api.updateItem(1, item); }) .then(update => { return api.deleteItem(1); }) .catch(e => { console.log('error while working on item 1'); })
各
then()
呼び出しは、Promiseのチェーンに別のステップを作成します。 チェーン内のどこかでエラーが発生すると、失敗したセクションの背後にある
catch()
ブロックが呼び出されます。
then()
および
catch()
メソッドは、何らかの値または新しいpromiseを返すことができ、結果はチェーン内の次の
then()
演算子に渡されます。
ここで、比較のために、コールバックを使用した同じロジックの実装:
api.getItem(1, (err, data) => { if (err) throw err; item.amount++; api.updateItem(1, item, (err, update) => { if (err) throw err; api.deleteItem(1, (err) => { if (err) throw err; }) }) })
このコードと前のコードの最初の違いは、コールバックの場合、単一のブロックを使用してすべてのエラーを処理するのではなく、プロセスの各ステップでエラー処理を含める必要があることです。 コールバックの2番目の問題は、スタイルに関するものです。 各ステップを表すコードブロックは水平方向に整列しているため、約束に基づいてコードを見たときに明らかな一連の操作を知覚することは困難です。
callbackコールバックをプロミスに変換する
コールバックからプロミスに切り替えるときに学ぶ最初のコツの1つは、コールバックをプロミスに変換することです。 たとえば、コールバックをまだ使用しているライブラリを使用する場合、またはそれらを使用して記述された独自のコードを使用する場合、これが必要になる可能性があります。 コールバックからプロミスへの移行はそれほど難しくありません。 次に、
fs.readFile
ベースのNode
fs.readFile
関数を
fs.readFile
を使用する関数に変換する例を示し
fs.readFile
。
function readFilePromise(filename) { return new Promise((resolve, reject) => { fs.readFile(filename, 'utf8', (err, data) => { if (err) reject(err); else resolve(data); }) }) } readFilePromise('index.html') .then(data => console.log(data)) .catch(e => console.log(e))
この機能の基礎は
Promise
コンストラクターです。 関数を使用します。この関数には、
resolve
と
reject
2つのパラメーターがあり、これらも関数です。 この関数内では、すべての作業が完了し、完了すると、
resolve
呼び出され、エラーが発生
reject
場合は
reject
ます。
その結果、1つのことを
resolve
必要があることに注意してください
resolve
または
reject
いずれかであり、この呼び出しは1回だけ行う必要があります。 この例では、
fs.readFile
がエラーを返した場合、このエラーを
reject
渡します。 それ以外の場合は、ファイルデータを渡して
resolve
し
resolve
。
Values値を約束に変換する
ES6には、通常の値からプロミスを作成するための便利なヘルパー関数がいくつかあります。 これらは
Promise.resolve()
および
Promise.reject()
です。 たとえば、promiseを返す必要があるが、いくつかのケースを同期的に処理する関数があるとします。
function readFilePromise(filename) { if (!filename) { return Promise.reject(new Error("Filename not specified")); } if (filename === 'index.html') { return Promise.resolve('<h1>Hello!</h1>'); } return new Promise((resolve, reject) => {/*...*/}) }
Promise.reject()
呼び出すときに何でも(または何でも)渡すことができますが、このメソッドには常に
Error
オブジェクトを渡すことをお勧めします。
promise約束の同時実行
Promise.all() —
、
Promise.all() —
配列を同時に実行するための便利なメソッドです。 たとえば、ディスクから読み取りたいファイルのリストがあるとしましょう。 前に作成した
readFilePromise
関数を使用すると、この問題の解決策は次のようになります。
let filenames = ['index.html', 'blog.html', 'terms.html']; Promise.all(filenames.map(readFilePromise)) .then(files => { console.log('index:', files[0]); console.log('blog:', files[1]); console.log('terms:', files[2]); })
従来のコールバックを使用して同等のコードを記述しようとさえしません。 そのようなコードは混乱を招きやすく、エラーが発生しやすいと言うだけで十分です。
▍一貫した約束
いくつかの約束を同時に実行すると、トラブルが発生する場合があります。 たとえば、
Promise.all
を使用してAPIから多くのリソースを取得しようとすると、これはAPIであり、しばらくしてからアクセス頻度の制限を超えると、 429エラーが生成される可能性が非常に高くなります 。
この問題の解決策の1つは、Promiseを順番に実行することです。 残念ながら、ES6には、このような操作を実行するための
Promise.al
lの単純な類似物はありません(理由を知りたいのですが)。しかし、ここではArray.reduceメソッドが役立ちます。
let itemIDs = [1, 2, 3, 4, 5]; itemIDs.reduce((promise, itemID) => { return promise.then(_ => api.deleteItem(itemID)); }, Promise.resolve());
この場合、次の呼び出しを行う前に、
api.deleteItem()
現在の呼び出し
api.deleteItem()
するのを待ちます。 このコードは、そうでなければ各要素識別子に対して
then()
を使用して書き換える必要がある操作を処理する便利な方法を示しています。
Promise.resolve() .then(_ => api.deleteItem(1)) .then(_ => api.deleteItem(2)) .then(_ => api.deleteItem(3)) .then(_ => api.deleteItem(4)) .then(_ => api.deleteItem(5));
▍プロミスレース
ES6で使用できるもう1つの便利なヘルパー関数は(ほとんど使用しませんが)
Promise.race
です。
Promise.all
と同様に、
Promise.all
の配列を受け入れて同時に実行しますが、
Promise.all
いずれかが実行または拒否されるとすぐに返されます。 他の約束の結果は破棄されます。
たとえば、しばらくすると失敗するプロミスを作成し、別のプロミスで表されるファイルの読み取り操作に制限を設定します。
function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(reject, ms); }) } Promise.race([readFilePromise('index.html'), timeout(1000)]) .then(data => console.log(data)) .catch(e => console.log("Timed out after 1 second"))
他の約束は引き続き実行されることに注意してください-単に結果が表示されません。
▍エラートラップ
.catch()
エラーをキャッチする通常の方法は、チェーンの最後に
.catch()
ブロックを追加することです。これにより、前の
.then()
ブロックのいずれかで発生するエラーをキャッチします。
Promise.resolve() .then(_ => api.getItem(1)) .then(item => { item.amount++; return api.updateItem(1, item); }) .catch(e => { console.log('failed to get or update item'); })
getItem
または
updateItem
いずれかが失敗すると、
catch()
ブロックがここで呼び出されます。 しかし、共同エラー処理が不要で、
getItem
で発生したエラーを個別に処理する必要がある場合は
getItem
でしょうか。 これを行うには、
getItem —
呼び出しでブロックの直後に別の
catch()
ブロックを挿入します。別のプロミスを返すこともできます。
Promise.resolve() .then(_ => api.getItem(1)) .catch(e => api.createItem(1, {amount: 0})) .then(item => { item.amount++; return api.updateItem(1, item); }) .catch(e => { console.log('failed to update item'); })
ここで、
getItem()
が失敗した場合、ステップインして新しい要素を作成します。
errors投げエラー
then()
式内のコードは、
try
ブロック内にあるかのように解釈する必要があります。
return Promise.reject()
を
return Promise.reject()
呼び出しと、
throw new Error()
を
throw new Error()
する呼び出しの両方が、次の
catch()
ブロックを実行します。
これは、実行時エラーも
catch()
ブロックをトリガーすることを意味します。そのため、エラー処理に関しては、ソースについて推測するべきではありません。 たとえば、次のコードフラグメントでは、
catch()
ブロックは
getItem
が動作したときに発生したエラーを処理するためだけに呼び出されることが期待できますが、例が示すように、
then()
式内で発生するランタイムエラーにも応答します:
api.getItem(1) .then(item => { delete item.owner; console.log(item.owner.name); }) .catch(e => { console.log(e); // Cannot read property 'name' of undefined })
▍ダイナミックプロミスチェーン
場合によっては、約束の連鎖を動的に構築する必要があります。つまり、特定の条件が満たされたときに追加のステップを追加する必要があります。 次の例では、指定されたファイルを読み取る前に、必要に応じてロックファイルを作成します。
function readFileAndMaybeLock(filename, createLockFile) { let promise = Promise.resolve(); if (createLockFile) { promise = promise.then(_ => writeFilePromise(filename + '.lock', '')) } return promise.then(_ => readFilePromise(filename)); }
そのような状況では、形式
promise = promise.then(/*...*/)
構造を使用して
promise
値を更新する必要があります。 この例に関連するのは、「Multiple Calls .then()」セクションで後述することです。
アンチパターン
約束はきちんとした抽象化ですが、それらと連携することは落とし穴に満ちています。 ここで、Promiseを操作するときに遭遇した典型的な問題をいくつか検討します。
地獄のコールバックの再構築
コールバックからプロミスに移行し始めたばかりのとき、古い習慣を放棄するのは難しいことがわかり、コールバックのようにお互いにプロミスを入れることに気付きました。
api.getItem(1) .then(item => { item.amount++; api.updateItem(1, item) .then(update => { api.deleteItem(1) .then(deletion => { console.log('done!'); }) }) })
実際には、そのような設計はほとんど必要ありません。 ネストの1つまたは2つのレベルが関連タスクのグループ化に役立つ場合がありますが、ネストされたプロミスはほとんどの場合、
.then()
構成される垂直チェーンとして書き換えられます。
return戻りコマンドがありません
私が遭遇した一般的で有害なエラーは、一連の約束の中で
return
呼び出しを忘れている
return
です。 たとえば、このコードでエラーを見つけることができますか?
api.getItem(1) .then(item => { item.amount++; api.updateItem(1, item); }) .then(update => { return api.deleteItem(1); }) .then(deletion => { console.log('done!'); })
エラーは、4行目の
api.updateItem
前に
return
呼び出しを配置しなかった
return
です。この特定の
then()
ブロックはすぐに解決されます。 その結果、
api.updateItem()
前に
api.updateItem()
呼び出される可能性があります。
私の意見では、これはES6の約束の大きな問題であり、予測できない動作につながることがよくあります。 問題は、
then()
が値または新しい
Promise
オブジェクトを返す一方で、
undefined
返す可能性があることです。 個人的に、JavaScript Promises APIを担当している場合、
.then()
ブロックが
undefined
返した場合、ランタイムエラーが発生します。 ただし、このようなことは言語には実装されていないため、注意して、作成したプロミスから明示的に復帰する必要があります。
。複数の.then()呼び出し
ドキュメントによると、同じ約束で
.then()
何度も呼び出すことができ、コールバックは登録されたのと同じ順序で呼び出されます。 しかし、そうする本当の理由を見たことはありません。 そのようなアクションは、promiseによって返された値を使用したり、エラーを処理したりするときに、理解できない結果をもたらす可能性があります。
let p = Promise.resolve('a'); p.then(_ => 'b'); p.then(result => { console.log(result) // 'a' }) let q = Promise.resolve('a'); q = q.then(_ => 'b'); q = q.then(result => { console.log(result) // 'b' })
この例では、
then()
呼び出されたときに
p
の値を更新していないため、
'b'
返されることはありません。 Promis
q
より予測可能
q
。then
then()
呼び出すことで毎回更新します。
同じことがエラー処理にも当てはまります。
let p = Promise.resolve(); p.then(_ => {throw new Error("whoops!")}) p.then(_ => { console.log('hello!'); // 'hello!' }) let q = Promise.resolve(); q = q.then(_ => {throw new Error("whoops!")}) q = q.then(_ => { console.log('hello'); // })
ここでは、Promiseのチェーンの実行を中断するエラーが予想されますが、
p
の値は更新されないため、2番目の
then()
ます。
.then()
を複数回呼び出すと、元のプロミスからいくつかの新しい独立したプロミスを作成できますが、この効果の実際のアプリケーションを見つけることができませんでした。
callbackコールバックとプロミスの混合
Promiseベースのライブラリを使用しているが、コールバックベースのプロジェクトで作業している場合、別のanotherに陥りやすいです。
then()
または
catch() —
ブロックからコールバックを呼び出さないようにします。そうしないと、promiseは次のエラーをすべて吸収し、promiseチェーンの一部として扱います。 以下に、コールバックでプロミスをラップする例を示します。一見したところ、実際の使用に非常に適しているように思えます。
function getThing(callback) { api.getItem(1) .then(item => callback(null, item)) .catch(e => callback(e)); } getThing(function(err, thing) { if (err) throw err; console.log(thing); })
ここでの問題は、エラーが発生した場合、
catch()
ブロックがチェーン内に存在するにもかかわらず、「未処理のプロミスの拒否」という警告が表示されることです。 これは、
callback()
が
then()
内と
catch()
内の両方で呼び出され、Promiseのチェーンの一部になるためです。
コールバックでプロミスを絶対にラップする必要がある場合は、
setTimeout
関数、またはNode.jsの
process.nextTick
を使用してプロミスを終了できます。
function getThing(callback) { api.getItem(1) .then(item => setTimeout(_ => callback(null, item))) .catch(e => setTimeout(_ => callback(e))); } getThing(function(err, thing) { if (err) throw err; console.log(thing); })
aughtエラーをキャッチ
JavaScriptエラー処理は奇妙なことです。 これは、古典的な
try/catch
パラダイムをサポートしますが、たとえばJavaで行われるように、それを呼び出す構造によって呼び出されるコードでのエラー処理をサポートしません。 ただし、JSでは、コールバックの使用が一般的であり、その最初のパラメーターはエラーオブジェクトです(このようなコールバックはerrbackとも呼ばれます)。 これにより、メソッドを呼び出すコンストラクトは、少なくともエラーの可能性を考慮します。
fs
ライブラリの例を次に示します。
fs.readFile('index.html', 'utf8', (err, data) => { if (err) throw err; console.log(data); })
Promiseを使用する場合、エラーを明示的に処理する必要があることを忘れがちです。 これは、ファイルシステムを操作するコマンドやデータベースにアクセスするコマンドなど、エラーの影響を受けやすい操作に関して特に当てはまります。 現在の状況では、拒否されたプロミスを傍受しない場合、Node.jsでかなり見苦しい警告が表示されます。
(node:29916) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: whoops! (node:29916) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
これを回避するには、promiseのチェーンの最後に
catch()
を追加することを忘れないでください。
まとめ
約束を使用するいくつかのパターンと反パターンを見ました。 ここで何かお役に立てば幸いです。 ただし、Promiseのトピックは非常に広範囲にわたるため、追加リソースへのリンクをいくつか示します。
- Mozilla Promiseの資料
- Googleからの約束の紹介
- Dave Atchleyによるレビューの約束
- あちこち -パターンおよびアンチパターンに関する追加資料
親愛なる読者! Node.jsプロジェクトでプロミスをどのように使用しますか?