ES6の約束:パターンとアンチパターン

数年前、Node.jsで働き始めたとき、「 コールバック地獄 」と呼ばれるものが怖くなりました しかし、この地獄から抜け出すのはそれほど簡単ではありませんでした。 ただし、現在Node.jsには最新の最も興味深いJavaScript機能が含まれています。 特に、 バージョン4以降のNodeはプロミスをサポートしています。 コールバックで構成される複雑なデザインから逃れることができます。



画像



コールバックの代わりにプロミスを使用すると、読みやすいより簡潔なコードを作成できます。 ただし、それらに精通していない人にとっては、特に明確に見えないかもしれません。 この記事では、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のトピックは非常に広範囲にわたるため、追加リソースへのリンクをいくつか示します。





親愛なる読者! Node.jsプロジェクトでプロミスをどのように使用しますか?



All Articles