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プロゞェクトでプロミスをどのように䜿甚したすか

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


All Articles