再び玄束に぀いお

倚くの玄束に぀いお曞かれおいたす。 この蚘事は、Promiseを䜿甚するための最も実甚的なトリックず、これがどのように機胜するかに぀いおの合理的に詳现な説明をたずめる詊みにすぎたせん。


玄束の抂芁


たず、いく぀かの定矩。


プロミスは、非同期呌び出しの実行を効率化するオブゞェクトです。


非同期呌び出しは、メむンコヌドストリヌムの実行が呌び出しの終了を埅たない関数呌び出しです。 たずえば、http芁求はメむンスレッドを䞭断したせん。 ぀たり、芁求が実行され、その完了を埅たずにすぐに、この呌び出しに続くコヌドが実行され、http芁求の結果が完了埌にコヌルバック関数コヌルバック関数によっお凊理されたす。


次に、promiseの機胜を順番に凊理したす。 ここでは、特定の非同期呌び出しを行うためのpromiseオブゞェクトが既にあるずいう事実から進めたす。 玄束がどこから来たのか、そしおそれを自分でどのように圢成するのかに぀いおは埌で話したす。



1. Promiseは、非同期呌び出しのシヌケンスを制埡するメカニズムを提䟛したす。 ぀たり、promiseは非同期呌び出しの単なるラッパヌです。 玄束は正垞に解決される堎合ず倱敗する堎合がありたす。 メカニズムの本質は次のずおりです。 各プロミスは、thenずcatchの2぀の関数を提䟛したす。 コヌルバック関数は、匕数ずしお各関数に枡されたす。 次に、promiseが正垞に解決された堎合にコヌルバック関数が呌び出され、゚ラヌの堎合にcatchにコヌルされたす


promise.then(function(result){ //      }).catch(function(error){ //     }); 

2.次に理解すべき点。 玄束は䞀床だけ蚱可されたす。 ぀たり、promise内で発生する非同期呌び出しは1回だけ実行されたす。 将来的には、promiseは非同期呌び出しの保存された結果を返すだけで、それを実行しなくなりたす。 たずえば、玄束の玄束がある堎合


 //    promise.then(function(result){ //      , //       }); //      promise.then(function(result){ //      . //       //   }); 

3. thenおよびcatch関数はpromiseを返したす。 したがっお、玄束のチェヌンを構築できたす。 この堎合、コヌルバック関数がthenで返す結果は、埌続のthenのコヌルバック関数の入力ぞの匕数ずしお枡されたす。 たた、throwメッセヌゞ呌び出しの匕数は、catchのコヌルバック関数の匕数ずしお枡されたす。


 promise.then(function(result){ //    ( ) return result1; //     }).then(function(result){ //  result      result1, //     if(condition){ throw("Error message"); } }).catch(function(error){ //      , //   ,      //    error    "Error message" }); 

4.その結果、thenのコヌルバック関数は、䜕らかの倀だけでなく、別のプロミスも返すこずができたす。 この堎合、このプロミスを解決した結果は、thenの次の呌び出しに転送されたす。


 promise1.then(function(result1){ return promise2.then(function(result2){ // -  return result3; }); }).then(function(result){ //   result      result3 }).catch(function(error){ //   }); 

5.アタッチメントなしの1぀の線圢チェヌンでプロミスの解決を構築するこずにより、同じこずを行うこずができたす。


 promise1.then(function(result1){ return promise2; }).then(function(result2){ // -  return result3; }).then(function(result){ //   result      result3 }).catch(function(error){ //   }); 

各promiseチェヌンはcatch呌び出しで終了するこずに泚意しおください。 これを行わないず、発生した゚ラヌは玄束の腞内で消え、゚ラヌが発生した堎所を特定するこずはできなくなりたす。


たずめるず


非同期呌び出しのシヌケンスを䜜成する必芁がある堎合぀たり、前の呌び出しが完了した埌、次の各呌び出しを厳密に完了する必芁がある堎合、これらの非同期呌び出しを䞀連の玄束に配眮する必芁がありたす。


玄束はどこから来るのですか


玄束は2぀の方法で受け取るこずができたす。䜿甚するラむブラリの関数が既成の玄束を返すか、玄束をサポヌトしない関数ぞの非同期呌び出しを個別に玄束に倉えるこずができたす玄束。 各オプションを個別に怜蚎したす。


ラむブラリ関数の玄束


倚くの最新ラむブラリは、Promiseを䜿甚した䜜業をサポヌトしおいたす。 ラむブラリがpromiseをサポヌトしおいるかどうかは、ドキュメントから確認できたす。 䞀郚のラむブラリは、コヌルバック関数ずプロミスずいう非同期呌び出しを凊理するための䞡方のオプションをサポヌトしおいたす。 人生の䟋をいく぀か考えおみたしょう。


りォヌタヌラむンORMデヌタベヌスラむブラリ


ドキュメントには、ラむブラリ関数の䜿甚䟋が蚘茉されおいたすデヌタサンプリングの䟋を䜿甚。


 Model.find({id: [1,2,3]}).exec(function(error, data){ if(error){ //   } else{ //   ,   data } }); 

すべおが矎しいようです。 ただし、デヌタを遞択しお凊理した埌、デヌタで他の操䜜を行う必芁がある堎合たずえば、あるテヌブルのレコヌドを曎新し、別のテヌブルに新しいレコヌドを挿入する、コヌルバック関数のネストは、コヌドの可読性が急激に䜎䞋する傟向があるようになりたすれロ


 Model.find({id: [1,2,3]}).exec(function(error, data){ if(error){ //   } else{ //   ,   data Model_1.update(...).exec(function(error, data){ if(error){/*   */} else{ //   Model_2.insert(...).exec(function(error, data){ //  .. }); } }); } }); 

䞎えられた䟋にメむンコヌドがない堎合でも、䜕が曞かれたかを理解するこずはすでに非垞に困難です。 たた、デヌタベヌスク゚リをルヌプで実行する必芁がある堎合、コヌルバック関数を䜿甚するず、タスクは通垞解決できなくなりたす。


ただし、Waterline ORMラむブラリの関数はpromiseで機胜したすが、これは䜕らかの方法でドキュメントに蚘茉されおいたす。 ただし、Promiseを䜿甚するず、人生が倧幅に簡玠化されたす。 デヌタベヌスク゚リ関数がプロミスを返すこずがわかりたした。 そしおこれは、玄束の蚀語の最埌の䟋を次のように曞くこずができるこずを意味したす。


 Model.find(...).then(function(data){ //   ,   data return Model_1.update(...); }).then(function(data){ //   return Model_2.insert(...); }).then(function(data){ //  . . }).catch(function(error){ //       }); 

どちらの゜リュヌションが優れおいるかを蚀う必芁はないず思いたす。 すべおの゚ラヌの凊理が1か所で行われるようになったこずは特に喜ばしいこずです。 垞に良いずは蚀えたせんが。 ゚ラヌのコンテキストから、発生した特定のブロックが明確ではないこずがあり、デバッグが耇雑になりたす。 この堎合、各promiseにcatch呌び出しを挿入するこずを犁止したせん。


 Model.find(...).then(function(data){ //   ,   data return Model_1.update(...).catch(function(error){...}); }).then(function(data){ //   return Model_2.insert(...).catch(function(error){...}); }).then(function(data){ //  . . }).catch(function(error){ ... }); 

この堎合でも、コヌドはコヌルバック関数を䜿甚するよりも理解しやすいです。


MongoDBを操䜜するためのラむブラリnode.jsのベヌスラむブラリ


前のケヌスず同様に、 ドキュメントの䟋ではコヌルバック関数を䜿甚しおいたす。


 MongoClient.connect(url, function(err, db) { assert.equal(null, err); console.log("Connected succesfully to server"); db.close(); }); 

前の䟋で既に芋おきたように、コヌルバックを䜿甚するず、倚数の非同期呌び出しを連続しお行う必芁がある堎合に非垞に困難になりたす。 幞いなこずに、ドキュメントを泚意深く調べおみるず、コヌルバック関数をこのラむブラリの関数ぞの匕数ずしお枡さない堎合、promiseが返されるこずがわかりたす。 したがっお、前の䟋は次のように曞くこずができたす。


 MongoClient.connect(url).then(function(db){ console.log("Connected succesfully to server"); db.close(); }).catch(function(err){ //   }); 

このラむブラリを䜿甚する特殊性は、デヌタベヌスぞのク゚リを実行するために、デヌタベヌス蚘述子オブゞェクトdbが䜿甚されるこずです。これは、connectぞの非同期呌び出しの結果ずしお返されたす。 ぀たり、䜜業䞭にデヌタベヌスに察しお倚くのク゚リを実行する必芁がある堎合、この蚘述子を毎回受信する必芁がありたす。 そしお、ここで、promiseを䜿甚するこずでこの問題が矎しく解決されたす。 これを行うには、デヌタベヌスに接続するずいう玄束を倉数に保存したす。


 var dbConnect = MongoClient.connect(url); //   //        dbConnect.then(function(db){ return db.collection('collection_name').find(...); }).then(function(data){ //     //   db   ,  //      return dbConnect.then(function(db){ return db.collection('collection_name').update(...); }); }).then(function(result){ //     update //  .. ... }).catch(function(error){ //   }); //        //      ,    //      dbConnect.then(function(db){ ... }).catch(function(error){ ... }); 

このようなコヌドの線成で良いのは、デヌタベヌスぞの接続が䞀床確立されるこずです玄束は䞀床だけ蚱可され、保存された結果が単に返されるこずを思い出したす。 ただし、䞊蚘の䟋には1぀の問題がありたす。 デヌタベヌスぞのすべおのク゚リを完了した埌、デヌタベヌスぞの接続を閉じる必芁がありたす。 デヌタベヌスぞの接続を閉じるこずは、Promiseチェヌンの最埌に挿入できたす。 ただし、起動した2぀のチェヌンのどちらが早く終了するかはわかりたせん。぀たり、2぀のチェヌンのどちらをベヌスぞの接続の最埌に挿入するかが明確ではありたせん。 Promise.allを呌び出すず、この問題を解決できたす。 圌に぀いおは少し埌で説明したす。


それたでの間、Promiseを䜿甚しお非同期呌び出しをルヌプで線成する方法に぀いおの議論は延期したす。 結局、前述のように、コヌルバックを䜿甚しおもこの問題をたったく解決できたせん。


ここで、ラむブラリがサポヌトしおいない堎合に自分でプロミスを䜜成する方法の質問に移りたしょう。


玄束をする玄束


ラむブラリがプロミスをサポヌトしおいないこずがよくありたす。 そしお、コヌルバックのみを䜿甚するこずをお勧めしたす。 小さなスクリプトず単玔なロゞックの堎合、これで満足できたす。 しかし、ロゞックが耇雑な堎合は、玄束なしにはできたせん。 そのため、Promiseで非同期呌び出しをラップできる必芁がありたす。


䟋ずしお、redisリポゞトリから属性倀を取埗するために、非同期関数でプロミスをラップするこずを怜蚎しおください。 redisラむブラリヌ関数はpromiseをサポヌトせず、コヌルバック関数でのみ機胜したす。 ぀たり、暙準の非同期関数呌び出しは次のようになりたす。


 var redis = require('redis').createClient(); redis.get('attr_name', function(error, reply){ if(error){ //  } else{ //   } }); 

次に、この関数をpromiseでラップしたす。 JavaScriptは、このためのPromiseオブゞェクトを提䟛したす。 ぀たり、新しい玄束を䜜成するには、これを行う必芁がありたす。


 new Promise(function(resolve, reject){ //     }); 

関数は匕数ずしおPromiseコンストラクタヌに枡され、その内郚で非同期呌び出しが行われたす。 この関数の匕数は、resolveおよびreject関数です。 非同期呌び出しが成功した堎合、解決関数を呌び出す必芁がありたす。 非同期呌び出しの結果は、resolve関数の入力に枡されたす。 ゚ラヌが発生した堎合は、拒吊関数を呌び出す必芁がありたす。 ゚ラヌは拒吊関数の入力に枡されたす。


すべおをたずめるず、getattr関数が取埗され、redisリポゞトリからパラメヌタヌを取埗するためのプロミスが返されたす。


 var redis = require('redis').createClient(); function get(attr){ return new Promise(function(resolve, reject){ redis.get(attr, function(error, reply){ if(error){ reject(error); } else{ resolve(reply); } }); }); } 

以䞊です。 これで、promiseに察しお通垞の方法でget関数を安党に䜿甚できたす。


 get('attr_name').then(function(reply){ //    }).catch(function(error){ //   }); 

なぜこのすべおの苊しみ


正気な人なら誰でもこの質問をするでしょう。 結局のずころ、私たちは今䜕をしおいるのでしょうか 非同期関数呌び出しを順番に実行する方法を説明しおきたした。 「通垞の」非同期呌び出しで動䜜しないプログラミング蚀語では、これは単に呜什を順番に曞くこずによっお行われたす。 玄束は必芁ありたせん。 そしお、単玔な䞀連の呌び出しを行うには、非垞に倚くの苊痛がありたす。 たぶん、非同期呌び出しはたったく必芁ありたせんか それらのために非垞に倚くの問題がありたす


しかし、銀の裏地はありたせん。 非同期呌び出しが存圚する堎合、呌び出しを行う方法を遞択できたす順次たたは䞊列。 ぀たり、あるアクションの実行に別のアクションの結果の存圚が必芁な堎合、promiseでthen呌び出しを䜿甚したす。 耇数のアクションの実行が互いに独立しおいる堎合、これらのアクションを䞊行しお開始できたす。


いく぀かの独立したアクションの䞊列実行を開始する方法は これを行うには、次の呌び出しを䜿甚したす。


 Promise.all([ promise_1, promise_2, ..., promise_n ]).then(function(results){ // -    }); 

぀たり、promiseの配列がPromise.all関数の入力に枡されたす。 この堎合のすべおの玄束はすぐに開始されたす。 Promise.all関数は、すべおのPromiseを履行するPromiseを返すため、この堎合のthenの呌び出しは、配列のすべおのPromiseが履行された埌にトリガヌされたす。 thenで関数の入力に枡されるresultsパラメヌタヌは、Promise.allの入力に枡された順序でのプロミスの結果の配列です。


もちろん、PromiseはPromise.allに倉えるこずはできず、単に順番に実行するだけです。 ただし、この堎合、すべおの玄束が完了する瞬間の制埡が倱われたす。


玄束をルヌプに保぀


倚くの堎合、サむクル内で䞀貫しお玄束を果たすずいうタスクがありたす。 ここでは、promiseが䞊列的な履行を蚱可する堎合、promise.allの入力に枡すたたはmap、filterなどの関数を䜿甚しおルヌプでpromiseの配列を構成するこずは簡単ではないため、promiseの順次履行を正確に怜蚎したす。


では、どのように玄束をルヌプに保぀のでしょうか 答えは簡単です-方法はありたせん。 ぀たり、厳密に蚀えば、サむクルで玄束を果たすこずはできたせんが、サむクルでは、次々に満たされる玄束のチェヌンを䜜るこずができたす。これは、呚期的な非同期アクションず本質的に同等です。


玄束の連鎖の埪環的構築のメカニズムの考察に目を向ける。 forルヌプずforEachルヌプずいう、すべおのプログラマヌに銎染みのあるタむプのルヌプを基に構築したす。


forルヌプでプロミスチェヌンを構築する


䜕らかのdoSomething関数が䜕らかの非同期アクションを実行する玄束を返し、このアクションをn回連続しお実行する必芁があるずしたす。 promise自䜓が、次の非同期呌び出しで䜿甚される文字列の結果を返すようにしたす。 たた、最初の呌び出しが空の文字列で行われたず仮定したす。 この堎合、䞀連の玄束の構築は次のように行われたす。


 //      var actionsChain = Promise.resolve(""); //      for(var i=0; i<n; i++){ actionsChain = actionsChain.then(function(result){ return doSomething(result); }); } // ,      //       actionsChain.then(function(result){ // ,       }).catch(function(error){ //   }); 

このサンプルテンプレヌトでは、説明が必芁なのは最初の行のみです。 残りは、䞊蚘の資料から明らかでなければなりたせん。 Promise.resolve関数は正垞に解決されたプロミスを返し、その結果はこの関数の匕数になりたす。 ぀たり、Promise.resolve ""は次ず同等です。


 new Promise(function(resolve, reject){ resolve(""); }); 

forEachルヌプでのプロミスチェヌンの構築


配列の各芁玠に察しお非同期アクションを実行する必芁があるずしたす。 この非同期アクションをdoSomething関数でラップするず、この非同期アクションを実行する玄束が返されたす。 この堎合、玄束の連鎖を構築する方法のテンプレヌトは、奇劙なこずに、forEach関数の䜿甚ではなく、reduce関数の䜿甚に基づいおいたす。


開始するには、reduce関数の動䜜を怜蚎しおください。 この関数は、䞭間結果を維持しながら配列芁玠を凊理するように蚭蚈されおいたす。 したがっお、最終的には、この配列の積分結果を埗るこずができたす。 たずえば、reduce関数を䜿甚しお、配列の芁玠の合蚈を蚈算できたす。


 var sum = array.reduce(function(sum, current){ return sum+current; }, 0); 

reduce関数の匕数は次のずおりです。


配列の各芁玠に察しお呌び出される関数。 この関数に枡される匕数は、前の呌び出しの結果、配列の珟圚の芁玠、この芁玠のむンデックス、および配列自䜓です䞊蚘の䟋では、最埌の2぀の匕数は必芁がないため省略されおいたす。

配列の最初の芁玠を凊理するずきに前のアクションの結果ずしお枡される初期倀。


次に、reduce関数を䜿甚しおタスクのPromiseのチェヌンを構築する方法を芋おみたしょう。


 array.reduce(function(actionsChain, value){ return actionsChain.then(function(){ return doSomething(value); }); }, Promise.resolve()); 

この䟋では、前のアクションの結果はactionsChainのプロミスであり、解決埌、doSomethingアクションを実行するための新しいプロミスが䜜成され、結果ずしお返されたす。 そのため、配列の各芁玠に察しおチェヌンが構築されたす。 Promise.resolveは、actionsChainの最初の玄束ずしお䜿甚されたす。


玄束を䜿甚するより耇雑な䟋


玄束を適甚するより耇雑な䟋ずしお、䞀意の識別子を生成するためのサヌビスのコヌドフラグメントを考えたす。 サヌビスの本質は非垞に単玔です。サヌビスに察するGET芁求ごずに、次の䞀意の識別子を蚈算しお返したす。 ただし、耇数の競合するリク゚ストを同時にサヌビスに送信できるずいう事実を考えるず、これらのリク゚ストを埌続の実行のためのプロミスのキュヌチェヌンに䞊べるずいうタスクが発生したす。


サヌバヌコヌドは次のようになりたす。


 //  HTTP- var server = http.createServer(processRequest); //       redis server.on('close', function(){ storage.quit(); }); //   server.listen(config.port, config.host); console.log('Service is listening '+config.host+':'+config.port); 

これは、node.jsで蚘述されたhttpサヌバヌの暙準コヌドです。 processRequest関数は、http芁求ハンドラヌです。 サヌビス自䜓はredisストレヌゞを䜿甚しおその状態を保存したす。 redisの状態持続メカニズムは、この䟋を理解するのに関係がないため、考慮されたせん。 ただし、サヌバヌがシャットダりンしたら、redisで接続を閉じる必芁がありたす。これは䞊蚘のコヌドに反映されおいたす。 唯䞀重芁なこずは、リク゚ストごずにprocessRequest関数が呌び出されるこずです。 芁求キュヌはrequestQueueの玄束です。 次に、コヌド自䜓を怜蚎したす。


 var requestQueue = Promise.resolve(); /** *    *      * @param {Object} req  * @param {Object} res  */ function processRequest(req, res){ requestQueue = requestQueue .then(function(){ //  GET-, ... if(req.method == 'GET'){ //  UUID  return calcUUID() //     .then(function(uuid){ res.writeHead(200, { 'Content-Type': 'text/plain', 'Cache-Control': 'no-cache' }); res.end(uuid); }) //   .catch(function(error){ console.log(error); res.writeHead(500, { 'Content-Type': 'text/plain', 'Cache-Control': 'no-cache' }); res.end(error); }); } //   GET-,   Bad Request else{ res.statusCode = 400; res.statusMessage = 'Bad Request'; res.end(); } }); } 

calcUUID(), . , . . , , .


おわりに


, , . .


, . - .



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


All Articles