玄束には問題がありたす。

ノヌラン・ロヌ゜ンの蚘事「 私たちは玄束に問題がありたす 」の翻蚳を玹介させおください。

玄束には問題がありたす。


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はこの問題を解決したすが、むンデントを枛らすだけではありたせん。 玠晎らしい䌚話「 地獄のコヌルバックからの救助 」で説明したように、圌らの本圓の問題は、私たちがリタヌンずスロヌの指瀺を䜿甚できないこずです。 代わりに、プログラムのロゞックは、ある関数が別の関数を呌び出すずきの副䜜甚の䜿甚に基づいおいたす。

コヌルバックは本圓に䞍吉なこずもしたすが、プログラミング蚀語では圓たり前だず思われおいるスタックを奪いたす。 スタックなしでコヌドを曞くこずは、ブレヌキペダルなしで車を運転するようなものです。 本圓に必芁で、適切な堎所に眮くたで、それがどれほど重芁かはわかりたせん。

玄束の芁点は、非同期ぞの移行時に倱われた蚀語の基本であるreturn 、 throw 、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を䜿甚するにはどうすればよいですか


これは、ほずんどの人が玄束を理解し始めた瞬間です。 圌らはforEach 、 for、たたは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぀のドキュメントが削陀されたずきに実行されたす。

これは非垞に油断のならない間違いです。特にドキュメントがむンタヌフェむスを曎新するのに十分な速さで削陀されおいる堎合は、最初は気付かないかもしれたせん。 バグは、すべおのブラりザではなく、たれなケヌスでのみ発生する可胜性がありたす。぀たり、バグを特定しお排陀するこずは事実䞊䞍可胜です。

芁玄するず、 forEach 、 for、および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を䜿甚するず、実際にはrejectHandlerはresolveHandler関数内で発生した゚ラヌをキャッチできないこずがわかりたす。

この機胜を知っお、 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); }); }); 

ずころで、 MochaずChaiは、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», . , :

— !

Source: https://habr.com/ru/post/J269465/


All Articles