約束には問題があります。

ノーラン・ローソンの記事「 私たちは約束に問題があります 」の翻訳を紹介させてください。



約束には問題があります。



JavaScript開発者の皆さん、これを認める時が来ました-約束に問題があります。



いいえ、約束そのものではありません。 A +仕様による実装優れています。 リッチAPIで苦労しているプログラマーの数を観察してきた長年にわたって、それ自体が私の前に現れた主な問題は次のとおりです。



「私たちの多くは、実際にそれらを理解せずに約束を使用します。」



私を信じないなら、この問題を解決してください:



質問:これら4つのPromiseの使用の違いは何ですか?



doSomething().then(function () { return doSomethingElse(); }); doSomething().then(function () { doSomethingElse(); }); doSomething().then(doSomethingElse()); doSomething().then(doSomethingElse);
      
      







あなたが答えを知っているなら、私はあなたを祝福させてください-あなたは約束に関する忍者です。 おそらく、あなたはこの投稿をこれ以上読むことができません。



あなたの残りの99.99%、私はあなたが動揺していない、あなたは良い会社にいると言って急いでいます。 私のツイートに答えた人は誰も問題を解決できませんでした。 3番目の質問への答えに私も非常に驚いた。 はい、私が彼に尋ねたという事実にもかかわらず!



この問題に対する答えは記事の最後にありますが、まず、最初の近似で約束がそれほど潜伏している理由と、初心者や専門家の多くがわなに陥る理由を説明したいと思います。 また、約束をよりよく理解するのに役立つ、珍しいトリックの1つであるソリューションも提供したいと思います。 そして、もちろん、私の説明の後、彼らはあなたにとってそれほど複雑に見えなくなると信じています。



始める前に、いくつかのポイントを概説しましょう。



なぜ約束するのか?



約束に関する記事を読むと、画面の右側にページを引き伸ばすひどいコールバックコードによって形成された「運命のピラミッド 」への参照がよく表示されます。



Promiseはこの問題を解決しますが、インデントを減らすだけではありません。 素晴らしい会話「 地獄のコールバックからの救助 」で説明したように、彼らの本当の問題は、私たちがリターンスローの指示を使用できないことです。 代わりに、プログラムのロジックは、ある関数が別の関数を呼び出すときの副作用の使用に基づいています。



コールバックは本当に不吉なこともしますが、プログラミング言語では当たり前だと思われているスタックを奪います。 スタックなしでコードを書くことは、ブレーキペダルなしで車を運転するようなものです。 本当に必要で、適切な場所に置くまで、それがどれほど重要かはわかりません。



約束の要点は、非同期への移行時に失われた言語の基本であるreturnthrow 、stackを返すことです。 ただし、これをより高いレベルに上げるには、Promiseの使用方法を知っている必要があります。



初心者の間違い



誰かが約束を漫画の形で、または言葉で説明しようとしています これはどこでも作成および送信できるものであり、非同期で受信および返される値を象徴します



この説明が十分に役立つとは思いません。 私にとって、約束は常にコード構造、その実行フローの一部です。



小さな余談:異なる人々に対する「約束」という用語は、異なる意味を持っています。 この記事では、最新のブラウザーでwindow.Promiseとして使用できる公式仕様について説明します。 window.Promiseを持たないブラウザの場合、仕様の最小限の実装を含む、生意気な名前Lie (false)を持つ適切なpolyfilがあります。



初心者の間違い#1-Promisesの「悪のピラミッド」



APIがpromiseに大きく結びついているPouchDBの使用方法を見ると、PouchDBを使用するための悪いパターンがたくさんあります。 最も一般的な例を次に示します。



 remotedb.allDocs({ include_docs: true, attachments: true }).then(function (result) { var docs = result.rows; docs.forEach(function(element) { localdb.put(element.doc).then(function(response) { alert("Pulled doc with id " + element.doc._id + " and added to local db."); }).catch(function (err) { if (err.status == 409) { localdb.get(element.doc._id).then(function (resp) { localdb.remove(resp._id, resp._rev).then(function (resp) { //   …
      
      





はい、約束がコールバックであるかのように使用できることがわかりました。はい、大砲からスズメを撃つのと同じです。



絶対的な初心者だけがそのような間違いを犯していると思うなら、私はあなたを驚かせるでしょう-上記のサンプルコードは公式のBlackBerry開発者ブログから取られます! コールバックを使用するという古い習慣を取り除くことは困難です。



より良いオプションは次のとおりです。



 remotedb.allDocs(...) .then(function (resultOfAllDocs) { return localdb.put(...); }) .then(function (resultOfPut) { return localdb.get(...); }) .then(function (resultOfGet) { return localdb.put(...); }) .catch(function (err) { console.log(err); });
      
      





上記の例では、複合約束(元の「約束の構成」)が使用されました-約束の長所の1つです。 後続の各関数は、前のpromiseが「解決」されるときに呼び出され、前のpromiseの結果とともに呼び出されます。 詳細は以下になります。



初心者#2エラー-PromisesでforEach()を使用するにはどうすればよいですか?



これは、ほとんどの人が約束を理解し始めた瞬間です。 彼らはforEachfor、またはwhileイテレータに精通していますが、それらをpromiseと組み合わせる方法がわかりません。 次に、このようなものが生まれます:



 //    remove()    db.allDocs({include_docs: true}) .then(function (result) { result.rows.forEach(function (row) { //  remove  promise db.remove(row.doc); }); }) .then(function () { //     ,     ! });
      
      





このコードの何が問題になっていますか? 問題は、最初の関数がundefinedを返すことです。つまり、2番目の関数は、すべてのドキュメントについてdb.remove()が完了するまで待機しません。 実際、それは何も期待せず、任意の数のドキュメント、またはおそらく1つのドキュメントが削除されたときに実行されます。



これは非常に油断のならない間違いです。特にドキュメントがインターフェイスを更新するのに十分な速さで削除されている場合は、最初は気付かないかもしれません。 バグは、すべてのブラウザではなく、まれなケースでのみ発生する可能性があります。つまり、バグを特定して排除することは事実上不可能です。



要約すると、 forEachfor、およびwhileのような構造体は、 「探しているドローンではない」と言いますPromise.all()が必要です



 db.allDocs({include_docs: true}) .then(function (result) { var arrayOfPromises = result.rows.map(function (row) { return db.remove(row.doc); }); return Promise.all(arrayOfPromises); }) .then(function (arrayOfResults) { //      ! });
      
      





ここで何が起こっていますか? Promise.all()は、 promiseの配列を引数として取り、すべてのドキュメントが削除された場合にのみ「解決」される新しいpromisを返します。 これはforループに相当する非同期です。



また、 Promise.all()からのpromiseは結果の配列を次の関数に渡します。これは、たとえば、ドキュメントを削除せずに複数のソースからデータを一度に取得する場合に非常に便利です。 Promise.all()に渡された配列から少なくとも1つのpromise 「オーバーライド」された場合、結果のpromiseは拒否状態になります。



初心者の間違い#3-.catch()の追加を忘れる



これもよくある間違いです-あなたの約束が決してエラーを返さないと信じることは至福です。 多くの開発者は、単にコードのどこかにcatch()を追加することを忘れています。



残念ながら、多くの場合、これはエラーが飲み込まれることを意味します。 あなたはそれらが何であるかさえ決して知ることはありません-アプリケーションをデバッグするときの特別な痛み。



この不愉快なシナリオを避けるために、私はそれをルールにしました。それは習慣に変わり、promiseのチェーンの最後に常にcatch()メソッドを追加しました。



 somePromise().then(function () { return anotherPromise(); }) .then(function () { return yetAnotherPromise(); }) //      : .catch(console.log.bind(console));
      
      





エラーがまったく発生しないことが保証されている場合でも、 catch()を追加することは賢明なソリューションです。 それから、もし突然あなたの間違いについての仮定が実現しないなら、あなたはあなたに感謝すると言うでしょう。



初心者#4エラー-遅延の使用



私はいつもこの間違いを目にします。同じ名前の映画のビートルジュースのように、使用回数が増えるのを待っているのではないかと心配しています。



要するに、約束は彼らの開発において長い道のりを歩んできました。 JavaScriptコミュニティは、それらを正しく実装するのに長い時間がかかりました。 当初、jQueryとAngularはあらゆる場所で遅延オブジェクトパターンを使用していましたが、後に「良い」ライブラリQ、When、RSVP、Bluebird、Lieなどに基づくES6 promises仕様に置き換えられました。



一般に、コード内でこの単語を突然書いた場合(3回繰り返しません)、何か間違ったことをしていることがわかります。 これを回避するためのレシピを次に示します。



ほとんどのプロミスショナルライブラリには、他のライブラリからプロミスをインポートするオプションがあります。 たとえば、Angularの$ qモジュールを使用すると、$ q以外のプロミスを$ q.when()でラップできます。 つまり、AngularユーザーはPouchDBからの約束を次のようにラップできます。



 //  ,  : $q.when(db.put(doc)).then(/* ... */);
      
      





別の方法は、オリジナルで「公開コンストラクターパターン」を使用することです。 promiseを使用しないAPIをラップするのに役立ちます。 たとえば、コールバックベースのNode.js APIをラップするには、次の操作を実行できます。



 new Promise(function (resolve, reject) { fs.readFile('myfile.txt', function (err, file) { if (err) { return reject(err); } resolve(file); }); }).then(/* ... */)
      
      





できた! 私たちはひどい延期に対処しました...ああ、私はほとんど3回言った! :)



初心者の間違い#5-結果を返す代わりに外部関数を使用する



このコードの何が問題になっていますか?



 somePromise().then(function () { someOtherPromise(); }) .then(function () { // ,   someOtherPromise «»… // , : ,  «». });
      
      





さて、今は約束について知る必要があるすべてについて話す絶好の機会です。



真剣に、これは同じトリックです。これを理解することで、あなたが私たちが話したすべての間違いを避けることができます。 準備はいいですか



先ほど述べたように、約束の魔法は、貴重なリターンスローを返すことです。 しかし、これは実際にはどういう意味ですか?



各promiseは、 then()メソッドを提供します(さらにcatch()も実際にはthen(null、...)の 「砂糖」です)。 そして、 then()関数の中にいます:



 somePromise().then(function () { // ,    then()! });
      
      





ここで何ができますか? 3つのこと:



  1. 別の約束を返す
  2. 同期値(またはundefined )を返します
  3. 同期エラーをスローする


それがすべて、全体のトリックです。 それを理解する-約束を理解する。 次に、各アイテムを詳細に分析します。



1.別の約束を返す



これは、上記の複合プロミスの例と同様に、プロミスに関するあらゆる種類の文献で見ることができる一般的なパターンです。



 getUserByName('nolan').then(function (user) { //  getUserAccountById  promise, //      then return getUserAccountById(user.id); }) .then(function (userAccount) { //     ! });
      
      





returnを使用して2番目のpromiseを正確に返すことに注意してください。 ここでreturnを使用することが重要です。 getUserAccountByIdを呼び出した場合、はい、ユーザーデータのリクエストがあり、どこでも役に立たない結果が得られます。次の結果は、目的のuserAccountではなくundefinedになります。



2.同期値を返します(または未定義)



結果として未定義を返すことはよくある間違いです。 ただし、同期値を返すことは、同期コードをPromiseのチェーンに変換するための優れた方法です。 メモリにユーザーデータのキャッシュがあるとします。 できること:



 getUserByName('nolan').then(function (user) { if (inMemoryCache[user.id]) { //     , //   return inMemoryCache[user.id]; } //       , //    return getUserAccountById(user.id); }) .then(function (userAccount) { //     ! });
      
      





かっこいいですね。 チェーンの2番目の関数は、データがどこから来たのか、キャッシュから、またはリクエストの結果としても関係なく、最初の関数はすぐに同期値または非同期プロミスを自由に返すことができます。



残念ながら、 returnを使用しなかった場合、関数は値を返しますが、ネストされた関数を呼び出した結果ではなく、そのような場合にデフォルトで返される無用なundefinedになります。



私自身のために、ルールを導入しました。このルールは習慣になりました。常に内部でreturnを使用するか、 throwを使用しエラーをスローします。 同じことをお勧めします。



3.同期エラーをスローする



だから私たちは投げに行きました。 ここで、約束はさらに明るくなり始めます。 ユーザーがログインしていない場合に同期エラーをスローするとします。 簡単です:



 getUserByName('nolan').then(function (user) { if (user.isLoggedOut()) { //   —  ! throw new Error('user logged out!'); } if (inMemoryCache[user.id]) { //     , //   return inMemoryCache[user.id]; } //       , //    return getUserAccountById(user.id); }) .then(function (userAccount) { //     ! }) .catch(function (err) { // , ,     ! });
      
      





catch()は、ユーザーが認証されていない場合は同期エラーを受け取り、上記の約束のいずれかが拒否状態になると非同期エラーを受け取ります。 繰り返しますが、エラーが同期的か非同期的かに関係なく、関数はキャッチします。



これは、開発中にエラーをキャッチするのに特に役立ちます。 たとえば、 JSONが無効な場合、 JSON.parse()を使用してthen()の内部のどこかにある文字列からオブジェクトを構築すると、エラーがスローされる場合があります。 コールバックでは、それは飲み込まれますが、 catch()メソッドを使用すると、簡単に処理できます。



高度なバグ



さて、あなたは約束の主なトリックを学んだので、極端なケースについて話しましょう。 常に極端なケースがあるからです。



プログラマーの約束に精通しているコードでのみエラーに出会ったため、このカテゴリのエラーを「高度」と呼びます。 この記事の冒頭で公開した問題を解析するには、このようなエラーについて議論する必要があります。



高度なエラー番号1-Promise.resolve()がわからない



非同期コードで同期ロジックをラップするときの約束がどれほど便利かはすでに上で示しました。 同様のことに気づいたかもしれません:



 new Promise(function (resolve, reject) { resolve(someSynchronousValue); }).then(/* ... */);
      
      





同じことをもっと短く書くことができることに留意してください:



 Promise.resolve(someSynchronousValue).then(/* ... */);
      
      





また、このアプローチは、同期エラーをキャッチするのに非常に便利です。 とても便利なので、約束を返すほとんどすべてのAPIメソッドで使用します。



 function somePromiseAPI() { return Promise.resolve() .then(function () { doSomethingThatMayThrow(); return 'foo'; }) .then(/* ... */); }
      
      





同期エラーをスローする可能性のあるコードは、「飲み込まれた」エラーによる潜在的なデバッグ問題であることを覚えておいてください。 しかし、 Promise.resolve()でラップすると、 catch()で確実にキャッチできます。



まだPromise.reject()があります。 拒否ステータスのプロミスを返すために使用できます:



 Promise.reject(new Error('-  '));
      
      





高度なエラー#2-catch()はthenと同じではありません(null、...)



少し前に、 catch()は単なる「砂糖」であると述べました。 以下の2つの例は同等です。



 somePromise().catch(function (err) { //   }); somePromise().then(null, function (err) { //   });
      
      





ただし、以下の例はもはや「同等」ではありません。



 somePromise().then(function () { return someOtherPromise(); }) .catch(function (err) { //   }); somePromise().then(function () { return someOtherPromise(); }, function (err) { //   });
      
      





上記の例がなぜ「等しくない」のか疑問に思ったら、最初の関数でエラーが発生した場合に何が起こるかを注意深く見てください。



 somePromise().then(function () { throw new Error('oh noes'); }) .catch(function (err) { //  ! :) }); somePromise().then(function () { throw new Error('oh noes'); }, function (err) { // ?  ? O_o });
      
      





format then(resolveHandler、rejectHandler)を使用すると、実際にはrejectHandlerresolveHandler関数内で発生したエラーをキャッチできないことがわかります。



この機能を知って、 then()メソッドで2番目の関数を決して使用せず、代わりに常にcatch()の形式でエラー処理を追加するルールを導入しました。 例外が1つだけあります。Mochaでの非同期テストです。意図的にエラーを待つ場合です。



 it('should throw an error', function () { return doSomethingThatThrows().then(function () { throw new Error('I expected an error!'); }, function (err) { should.exist(err); }); });
      
      





ところで、 MochaChaiは、PromiseベースのAPIをテストするための素晴らしい組み合わせです。



高度な間違い#3-約束と約束の工場



一連の約束を順番に完了したいとします。 Promise.all()のようなものが必要ですが、 promiseを並行して実行しないものが必要です。



それまでの間、次のように記述できます。



 function executeSequentially(promises) { var result = Promise.resolve(); promises.forEach(function (promise) { result = result.then(promise); }); return result; }
      
      





残念ながら、上記の例は意図したとおりには機能しません。 executeSequentially()に渡されたリストのPromiseは、引き続き並列実行を開始します。



その理由は、仕様によると、promiseは作成後すぐに埋め込まれたロジックの実行を開始するからです。 彼は待ちません。 したがって、promise自体ではなく、promiseファクトリの配列がexecuteSequentiallyに渡す必要があるものです。



 function executeSequentially(promiseFactories) { var result = Promise.resolve(); promiseFactories.forEach(function (promiseFactory) { result = result.then(promiseFactory); }); return result; }
      
      





あなたは今、 「このJavaプログラマは一体何者なのか、そしてなぜ彼が私たちに工場について語っているのか」と考えているのを知っています。 実際、ファクトリーはプロミスを返す単純な関数です。



 function myPromiseFactory() { return somethingThatCreatesAPromise(); }
      
      





この例が機能するのはなぜですか? しかし、私たちの工場は彼に順番が来るまで約束を作成しないからです。 then()の resolveHandlerとまったく同じように機能します。



関数executeSequentially()を注意深く見て、 promiseFactoryへのリンクをそのコンテンツでメンタルに置き換えます-これで、ライトが頭の上で楽しく点滅します:)



高度な間違い#4-2つの約束の結果が必要な場合はどうすればよいですか?



ある約束が別の約束に依存することがよくあり、最後には両方の結果が必要です。 例:



 getUserByName('nolan').then(function (user) { return getUserAccountById(user.id); }) .then(function (userAccount) { // ,     «user» ! });
      
      





優れたJavaScript開発者であり続けるために、「悪のピラミッド」が作成されないように、 ユーザー変数をより高いレベルの可視性にしたい場合があります



 var user; getUserByName('nolan').then(function (result) { user = result; return getUserAccountById(user.id); }) .then(function (userAccount) { // ,    «user»,  «userAccount» });
      
      





これは機能しますが、個人的にはコードが「スマッキング」していると思います。 私の決断は偏見を捨て、「ピラミッド」に向けて意識的な一歩を踏み出すことです。



 getUserByName('nolan').then(function (user) { return getUserAccountById(user.id) .then(function (userAccount) { // ,    «user»,  «userAccount» }); });
      
      





...少なくとも一時的なステップ。 インデントが増加し、ピラミッドが脅かされて成長し始めていると感じた場合は、JavaScript開発者が何世紀にもわたって行ってきたことを実行します。関数を作成し、名前で使用します。



 function onGetUserAndUserAccount(user, userAccount) { return doSomething(user, userAccount); } function onGetUser(user) { return getUserAccountById(user.id) .then(function (userAccount) { return onGetUserAndUserAccount(user, userAccount); }); } getUserByName('nolan') .then(onGetUser) .then(function () { //     doSomething() , //     —     });
      
      





コードが複雑になると、そのほとんどが名前付き関数に変換され、アプリケーションロジック自体が美的な喜びをもたらす形になり始めることがわかります。



 putYourRightFootIn() .then(putYourRightFootOut) .then(putYourRightFootIn) .then(shakeItAllAbout);
      
      





それが私たちの約束です。



高度な間違い5-約束を破る



, , . . , . , .



, ?



 Promise.resolve('foo') .then(Promise.resolve('bar')) .then(function (result) { console.log(result); });
      
      





, bar, . foo!



, , then() - (, ), then(null) «» . :



 Promise.resolve('foo') .then(null) .then(function (result) { console.log(result); });
      
      





then(null) , — foo.



. . , then() , , . then() . , - :



 Promise.resolve('foo') .then(function () { return Promise.resolve('bar'); }) .then(function (result) { console.log(result); });
      
      





bar, .



: then() .





, , , , , .



.



№1



 doSomething().then(function () { return doSomethingElse(); }) .then(finalHandler);
      
      





答えは:



 doSomething |-----------------| doSomethingElse(undefined) |------------------| finalHandler(resultOfDoSomethingElse) |------------------|
      
      





№2



 doSomething().then(function () { doSomethingElse(); }) .then(finalHandler);
      
      





答えは:



 doSomething |-----------------| doSomethingElse(undefined) |------------------| finalHandler(undefined) |------------------|
      
      





№3



 doSomething().then(doSomethingElse()) .then(finalHandler);
      
      





答えは:



 doSomething |-----------------| doSomethingElse(undefined) |---------------------------------| finalHandler(resultOfDoSomething) |------------------|
      
      





№4



 doSomething().then(doSomethingElse) .then(finalHandler);
      
      





答えは:



 doSomething |-----------------| doSomethingElse(resultOfDoSomething) |------------------| finalHandler(resultOfDoSomethingElse) |------------------|
      
      





, , doSomething() doSomethingElse() , . , - .



.





. , . , , , .



, - — PounchDB map/reduce . : 290 , 555 . -, … . .



, . , . , , . , - , . , , . . , , , , .



async/await



« ES7 » async/await , . - ( catch() , try/catch , , ES7 try/catch/return .



JavaScript, , - - , .



JavaScript, , JSLint JSHint , « JavaScript », . , , , , .



async/await , JS, - - . , , ES5 ES6.



« JavaScript», . , :



— !



All Articles