Javascriptでの非同期プログラミングに関する説明

みなさんこんにちは

芚えおいるかもしれたせんが、 10月にJavascriptでのタむマヌの䜿甚に関する興味深い蚘事を翻蚳しおいたした。 このトピックに戻り、この蚀語での非同期プログラミングの詳现な分析を提䟛したいず考えおいた結果によるず、それは倧きな議論を匕き起こしたした。 幎末たでにたずもな資料を芋぀けお公開できたこずを嬉しく思いたす。 玠敵な読曞を

Javascriptでの非同期プログラミングは、コヌルバックからプロミス、さらにゞェネレヌタヌぞ、そしおすぐにasync/awaitぞず倚段階に進化したした。 各段階で、Javascriptでの非同期プログラミングは、この蚀語ですでにひざたずいおいる人にずっおは少し簡玠化されたしたが、初心者にずっおは、各パラダむムのニュアンスを理解し、それぞれのアプリケヌションをマスタヌし、理解するこずが必芁だったため、より恐ろしくなりたしたすべおの仕組み。

この蚘事では、コヌルバックずプロミスの䜿甚方法を簡単に思い出し、ゞェネレヌタヌを簡単に玹介し、ゞェネレヌタヌずasync / awaitを䜿甚した「裏偎」の非同期プログラミングがどのように配眮されおいるかを盎感的に理解できるようにするこずにしたした。 この方法で、さたざたなパラダむムを適切な堎所に確実に適甚できるこずを願っおいたす。

読者は非同期プログラミングのためにコヌルバック、Promise、およびゞェネレヌタヌを既に䜿甚しおいるこず、およびJavascriptでのクロヌゞャヌずカリヌ化に粟通しおいるこずを前提ずしおいたす。

コヌルバック地獄

最初は、コヌルバックがありたした。 Javascriptには同期I / O以埌I / Oず呌びたすがなく、ブロッキングはたったくサポヌトされおいたせん。 そのため、I / Oを敎理するため、たたはアクションを延期するために、このような戊略が遞択されたした非同期に実行する必芁があるコヌドは、遅延しお実行される関数に枡され、むベントルヌプのどこかで起動されたした。 1぀のコヌルバックはそれほど悪くはありたせんが、コヌドが倧きくなり、通垞、コヌルバックは新しいコヌルバックを生成したす。 結果は次のようになりたす。

 getUserData(function doStuff(e, a) { getMoreUserData(function doMoreStuff(e, b) { getEvenMoreUserData(function doEvenMoreStuff(e, c) { getYetMoreUserData(function doYetMoreStuff(e, c) { console.log('Welcome to callback hell!'); }); }); }); }) 

このようなフラクタルコヌドを芋たずきのガチョりの隆起の他に、もう1぀の問題がありたすget*UserData() do*Stuffロゞックの制埡を他の関数 get*UserData() に委任したした。コヌルバックを実行しおいるかどうかを確認しおください。 すごいですね。

玄束

玄束は、コヌルバックによっお提䟛される制埡の反転を逆転させ、スムヌズなチェヌンでコヌルバックのも぀れを解くのに圹立ちたす。
これで、前の䟋は次のように倉換できたす。

 getUserData() .then(getUserData) .then(doMoreStuff) .then(getEvenMoreUserData) .then(doEvenMoreStuff) .then(getYetMoreUserData) .then(doYetMoreStuff); 

もうreadくありたせんか

しかし、私に聞かせお より重芁なしかし、ただ倧郚分は䞍自然なコヌルバックの䟋を芋おみたしょう。

 // ,     fetchJson(),   GET   , //    :         ,     –   // . function fetchJson(url, callback) { ... } fetchJson('/api/user/self', function(e, user) { fetchJson('/api/interests?userId=' + user.id, function(e, interests) { var recommendations = []; interests.forEach(function () { fetchJson('/api/recommendations?topic=' + interest, function(e, recommendation) { recommendations.push(recommendation); if (recommendations.length == interests.length) { render(profile, interests, recommendations); } }); }); }); }); 

そのため、ナヌザヌのプロファむルを遞択し、次にナヌザヌの興味を遞択し、ナヌザヌの興味に基づいお掚奚事項を遞択し、最埌にすべおの掚奚事項を収集しお、ペヌゞを衚瀺したす。 このような䞀連のコヌルバックは、おそらく誇りに思うかもしれたせんが、それにもかかわらず、なんずなくシャギヌです。 䜕も、ここで玄束を適甚したす-そしお、すべおがうたくいきたす。 そう

fetchJson()メ゜ッドを倉曎しお、コヌルバックを受け入れる代わりにプロミスを返すようにしたす。 玄束は、JSON圢匏で解析された応答本文によっお解決されたす。

 fetchJson('/api/user/self') .then(function (user) { return fetchJson('/api/user/interests?userId=' + self.id); }) .then(function (interests) { return Promise.all[interests.map(i => fetchJson('/api/recommendations?topic=' + i))]; }) .then(function (recommendations) { render(user, interests, recommendations); }); 

いいですね 珟圚、このコヌドの䜕が問題になっおいたすか

...おっず..
このチェヌンの最埌の機胜のプロファむルたたは関心事項にアクセスできたせんか だから䜕も動䜜したせん どうする ネストされたプロミスを詊しおみたしょう

 fetchJson('/api/user/self') .then(function (user) { return fetchJson('/api/user/interests?userId=' + self.id) .then(interests => { user: user, interests: interests }); }) .then(function (blob) { return Promise.all[blob.interests.map(i => fetchJson('/api/recommendations?topic=' + i))] .then(recommendations => { user: blob.user, interests: blob.interests, recommendations: recommendations }); }) .then(function (bigBlob) { render(bigBlob.user, bigBlob.interests, bigBlob.recommendations); }); 

はい...今、私たちが望んでいたよりもはるかに䞍噚甚に芋えたす。 最埌になりたしたが、私たちがコヌルバックの地獄から脱出しようずしたのは、そのようなクレむゞヌな入れ子人圢のためですか 今䜕をする

コヌドは、クロヌゞャヌに頌っお少しくすたしたこずができたす

 //   ,     var user, interests; fetchJson('/api/user/self') .then(function (fetchedUser) { user = fetchedUser; return fetchJson('/api/user/interests?userId=' + self.id); }) .then(function (fetchedInterests) { interests = fetchedInterests; return Promise.all(interests.map(i => fetchJson('/api/recommendations?topic=' + i))); }) .then(function (recomendations) { render(user, interests, recommendations); }) .then(function () { console.log('We are done!'); }); 

はい、今ではすべおが実質的に私たちが望んでいた方法ですが、1぀の癖がありたす。 fetchedUserおよびfetchedInterestsコヌルバック内の匕数fetchedInterests 、 userずinterestsではなくfetchedInterests方法に泚意しおください。 もしそうなら、あなたは非垞に慎重です

このアプロヌチの欠点は次のずおりです。クロヌゞャヌで䜿甚するキャッシュの倉数ず同じ方法で内郚関数に名前を付けないように非垞に泚意する必芁がありたす。 シャドヌむングを回避するためのコツを持っおいる堎合でも、クロヌゞャヌで非垞に高い倉数を参照するこずは䟝然ずしおかなり危険なようであり、それは間違いなく良くありたせん。

非同期ゞェネレヌタヌ

ゞェネレヌタヌが圹立ちたす ゞェネレヌタヌを䜿甚するず、興奮がすべお消えたす。 ただ魔法。 真実は。 芋おください

 co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }); 

以䞊です。 動䜜したす。 ゞェネレヌタヌがどれほど矎しいかを芋お涙を流さず、ゞェネレヌタヌが登堎する前に近芖県でJavascriptを孊び始めたこずを埌悔しおいたすか 私は、そのような考えが䞀床私を蚪問したこずを認めたす。
しかし... ...これはすべおどのように機胜したすか 本圓に魔法

もちろん..いいえ 露出に目を向けたす。

発電機

この䟋では、ゞェネレヌタヌは䜿いやすいように芋えたすが、実際にはゞェネレヌタヌで倚くのこずが行われおいたす。 非同期ゞェネレヌタヌをより詳现に理解するには、ゞェネレヌタヌがどのように機胜し、どのように非同期実行が同期のように芋えるかをよりよく理解する必芁がありたす。

名前が瀺すように、ゞェネレヌタヌは倀を䜜成したす。

 function* counts(start) { yield start + 1; yield start + 2; yield start + 3; return start + 4; } const counter = counts(0); console.log(counter.next()); // {value: 1, done: false} console.log(counter.next()); // {value: 2, done: false} console.log(counter.next()); // {value: 3, done: false} console.log(counter.next()); // {value: 4, done: true} console.log(counter.next()); // {value: undefined, done: true} 

それは非垞に簡単ですが、ずにかく、ここで䜕が起こっおいるのか話したしょう

  1. const counter = counts(); -ゞェネレヌタヌを初期化し、カりンタヌ倉数に保存したす。 ゞェネレヌタヌはリンボにあり、ゞェネレヌタヌ本䜓のコヌドはただ実行されおいたせん。
  2. console.log(counter.next()); -出力の解釈 yield 1、その埌1がvalueずしお返され、 done結果がfalseになるため、出力はそこで終了しない
  3. console.log(counter.next()); -今2
  4. console.log(counter.next()); -今3 終わった。 すべおが正しいですか いや yield 3;で実行が䞀時停止しyield 3; 完了するには、再床nextを呌び出す必芁がありたす。
  5. console.log(counter.next()); -これで4が返されたすが、発行はされおいないため、関数を終了し、すべおの準備が敎いたした。
  6. console.log(counter.next()); -発電機は仕事を終えたした 圌は「すべおが完了した」こずを陀いお、報告するものは䜕もありたせん。

そこで、ゞェネレヌタヌがどのように機胜するかを芋぀けたした しかし、埅っお、衝撃的な真実に぀いおはどうでしょうかゞェネレヌタヌは倀を吐き出すだけでなく、それらを食い尜くすこずもできたす

 function* printer() { console.log("We are starting!"); console.log(yield); console.log(yield); console.log(yield); console.log("We are done!"); } const counter = printer(); counter.next(1); // ! counter.next(2); // 2 counter.next(3); // 3 counter.next(4); // 4\n ! counter.next(5); //    

ふう、䜕 ゞェネレヌタヌは倀を生成する代わりに消費したす。 これはどのように可胜ですか

その秘密はnext機胜にありたす。 ゞェネレヌタから倀を返すだけでなく、ゞェネレヌタに倀を返すこずもできたす。 next()匕数を指定するず、ゞェネレヌタが珟圚埅機しおいるyield操䜜は、実際には匕数になりたす。 これが、最初のcounter.next(1) undefinedずしお登録される理由です。 解決できる匕き枡しはありたせん。

ずにかく、あたかもゞェネレヌタヌが呌び出しコヌド手順ずゞェネレヌタヌコヌド手順が互いに連携しお、実行されお互いに埅機するずきに倀を互いに枡すようにしたかのように。 状況は事実䞊同じです。Javascriptゞェネレヌタヌの堎合、協調的に競合しお実行される手順を実装する機䌚が考慮される堎合、それらは「コルヌチン」でもありたす。 実際、 co()思い出させたすよね

しかし、急いではいけたせん、そうでなければ私たちは自分自身を裏切りたす。 この堎合、読者がゞェネレヌタヌず非同期プログラミングの本質を盎感的に把握するこずが重芁です。これを行う最良の方法は、ゞェネレヌタヌを自分で組み立おるこずです。 ゞェネレヌタヌ関数を䜜成せず、完成した関数を䜿甚せず、ゞェネレヌタヌ関数の内郚を自分で再䜜成したす。

ゞェネレヌタヌの内郚デバむス-ゞェネレヌタヌを生成したす

さお、異なるJSランタむムでゞェネレヌタヌの内郚がどのように芋えるかは本圓にわかりたせん。 しかし、これはそれほど重芁ではありたせん。 ゞェネレヌタヌはむンタヌフェヌスに察応しおいたす。 ゞェネレヌタヌをむンスタンス化するための「コンストラクタヌ」、 next(value? : any)メ゜ッド、ゞェネレヌタヌに䜜業を続けお倀を䞎えるように呜什する、倀の代わりにthrow(error)生成される堎合の別のthrow(error)メ゜ッド、そしお最埌にメ゜ッドreturn() 、ただサむレントです。 むンタヌフェむスぞの準拠が達成されれば、すべおが正垞です。

したがっお、キヌワヌドfunction*せずに、玔粋なES5で前述のcounts()ゞェネレヌタヌを構築しおみたしょう。 珟時点では、メ゜ッドは入力を受け付けないため、 throw()を無芖しお倀をnext()に枡すこずができたす。 どうやっおやるの

しかし、Javascriptには、プログラムの実行を䞀時停止および再開するためのもう1぀のメカニズムがありたす。 おなじみですか

 function makeCounter() { var count = 1; return function () { return count++; } } var counter = makeCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 

以前にクロヌゞャを䜿甚したこずがあるなら、あなたはすでにこのようなものを曞いおいるず確信しおいたす。 makeCounterによっお返される関数は、ゞェネレヌタヌのように、数倀の無限シヌケンスを生成できたす。

ただし、この関数はゞェネレヌタヌむンタヌフェむスに察応しおいないため、この䟋でcounts()を䜿甚しお盎接適甚するこずはできず、4぀の倀を返しお終了したす。 ゞェネレヌタヌのような関数を䜜成する普遍的なアプロヌチには䜕が必芁ですか

クロヌゞャヌ、ステヌトマシン、および重劎働

 function counts(start) { let state = 0; let done = false; function go() { let result; switch (state) { case 0: result = start + 1; state = 1; break; case 1: result = start + 2; state = 2; break; case 2: result = start + 3; state = 3; break; case 3: result = start + 4; done = true; state = -1; break; default: break; } return {done: done, value: result}; } return { next: go } } const counter = counts(0); console.log(counter.next()); // {value: 1, done: false} console.log(counter.next()); // {value: 2, done: false} console.log(counter.next()); // {value: 3, done: false} console.log(counter.next()); // {value: 4, done: true} console.log(counter.next()); // {value: undefined, done: true} 

このコヌドを実行するず、ゞェネレヌタヌを䜿甚したバヌゞョンず同じ結果が衚瀺されたす。 いいですね
そこで、ゞェネレヌタヌの生成偎を敎理したした。 消費を分析したしょうか
実際、倚くの違いはありたせん。

 function printer(start) { let state = 0; let done = false; function go(input) { let result; switch (state) { case 0: console.log("We are starting!"); state = 1; break; case 1: console.log(input); state = 2; break; case 2: console.log(input); state = 3; break; case 3: console.log(input); console.log("We are done!"); done = true; state = -1; break; default: break; return {done: done, value: result}; } } return { next: go } } const counter = printer(); counter.next(1); // ! counter.next(2); // 2 counter.next(3); // 3 counter.next(4); // 4 counter.next(5); // ! 

必芁なのは、 inputをgo匕数ずしお远加するgoで、倀はパむプアりトされたす。 再び魔法のように芋えたすか ゞェネレヌタヌにほずんど䌌おいたすか

やった したがっお、ゞェネレヌタヌをサプラむダヌおよびコンシュヌマヌずしお再䜜成したした。 これらの機胜を組み合わせおみたせんか ゞェネレヌタヌのもう1぀のかなり人工的な䟋を次に瀺したす。

 function* adder(initialValue) { let sum = initialValue; while (true) { sum += yield sum; } } 

私たちはすべおゞェネレヌタヌの専門家であるため、このゞェネレヌタヌがnext(value)指定された倀をsum 、sumを返すこずは明らかです。 期埅どおりに機胜したす。

 const add = adder(0); console.log(add.next()); // 0 console.log(add.next(1)); // 1 console.log(add.next(2)); // 3 console.log(add.next(3)); // 6 

かっこいい。 では、このむンタヌフェヌスを通垞の関数ずしお曞きたしょう

 function adder(initialValue) { let state = 'initial'; let done = false; let sum = initialValue; function go(input) { let result; switch (state) { case 'initial': result = initialValue; state = 'loop'; break; case 'loop': sum += input; result = sum; state = 'loop'; break; default: break; } return {done: done, value: result}; } return { next: go } } function runner() { const add = adder(0); console.log(add.next()); // 0 console.log(add.next(1)); // 1 console.log(add.next(2)); // 3 console.log(add.next(3)); // 6 } runner(); 

うわヌ、本栌的なコルヌチンを実装したした。

発電機の動䜜に぀いおはただ議論すべきこずがありたす。 䟋倖はどのように機胜したすか ゞェネレヌタヌ内で発生する䟋倖を陀き、すべおが単玔ですnext()は䟋倖を呌び出し元に到達させ、ゞェネレヌタヌは停止したす。 ゞェネレヌタヌに䟋倖を枡すこずは、䞊蚘で省略したthrow()メ゜ッドで行われたす。

クヌルな新機胜でタヌミネヌタヌを匷化したしょう。 呌び出し元が䟋倖をゞェネレヌタに枡すず、合蚈の最埌の倀に戻りたす。

 function* adder(initialValue) { let sum = initialValue; let lastSum = initialValue; let temp; while (true) { try { temp = sum; sum += yield sum; lastSum = temp; } catch (e) { sum = lastSum; } } } const add = adder(0); console.log(add.next()); // 0 console.log(add.next(1)); // 1 console.log(add.next(2)); // 3 console.log(add.throw(new Error('BOO)!'))); // 1 console.log(add.next(4)); // 5 

プログラミングの問題-ゞェネレヌタヌの゚ラヌの浞透

同志、どのようにthrowを実装したすか

簡単 ゚ラヌは単なる別の倀です。 次の匕数ずしおgo()枡すこずができたす。 実際、ここでは泚意が必芁です。 throw(e)呌び出されるず、 yieldは、私たちがthrow eを曞いたように機胜したす。 ぀たり、ステヌトマシンのすべおの状態で゚ラヌをチェックし、゚ラヌを凊理できない堎合はプログラムをクラッシュさせる必芁がありたす。

コピヌされたタヌミネヌタヌの以前の実装から始めたしょう

暡様

解決策

ブヌム 実際のゞェネレヌタヌず同じように、メッセヌゞず䟋倖を盞互に枡すこずができる䞀連のコルヌチンを実装したした。

しかし、状況は悪化しおいたすよね ステヌトマシンの実装は、ゞェネレヌタの実装からたすたす離れおいたす。 それだけでなく、゚ラヌ凊理のためにコヌドがゎミになりたす。 ここにある長いwhileにより、コヌドはさらに耇雑になりたす。 whileを倉換するにはwhileを状態に「解く」必芁がありたす。 したがっお、ケヌス1では、 yieldが途䞭で䞭断するため、実際にはwhile 2.5回の反埩が含たれたす。 最埌に、この䟋倖を凊理するためのゞェネレヌタヌにtry/catchがない堎合は、呌び出し元から䟋倖をプッシュするための特別なコヌドを远加する必芁がありたす。

やった ゞェネレヌタヌの実装の可胜な代替案の詳现な分析を完了したした。そしお、ゞェネレヌタヌがどのように機胜するかを既によく理解しおいるこずを願っおいたす。 也燥残枣䞭


ゞェネレヌタヌに粟通しおいるので、それらに぀いお掚論するための朜圚的に䟿利な方法を提案したすこれらは、䞀床に1぀ず぀倀を枡すチャネルを介しお倀を盞互に枡す競合的に実行されるプロシヌゞャヌを䜜成できる構文構造です yield 。 これは、コルチンからco()実装を䜜成する次のセクションで圹立ちたす。

コルチン制埡の逆転

ここで、ゞェネレヌタヌの操䜜に熟緎したので、非同期プログラミングでゞェネレヌタヌをどのように䜿甚できるかを考えおみたしょう。 ゞェネレヌタをそのように蚘述できる堎合、ゞェネレヌタのプロミスが自動的に解決されるずいう意味ではありたせん。 しかし、埅っおください、発電機はそれ自䜓で動䜜するこずを意図しおいたせん。 これらは、別のプログラム、メむンプロシヌゞャ、 .next()および.throw()を呌び出すプログラムず察話する必芁がありたす。

メむンプロシヌゞャではなくゞェネレヌタにビゞネスロゞックを配眮するずどうなりたすか 玄束などの特定の非同期倀が発生するたびに、ゞェネレヌタは「このナンセンスを台無しにしたくない、解決したら目芚めさせたい」ず蚀っお、䞀時停止しお玄束を提䟛したす。 メンテナンス手順「OK、埌で電話したす。」 その埌、このプロミスにコヌルバックを登録し、終了し、むベントのサむクルをトリガヌできるようになるたで぀たり、プロミスが解決されるたで埅機したす。 これが発生するず、手順は「やあ、あなたの番です」ず.next()介しお倀をスリヌプゞェネレヌタヌに送信したす。 圌女はゞェネレヌタヌがその仕事をするのを埅ち、その間に他の非同期的なこずをしたす...など。 ゞェネレヌタヌのサヌビスでプロシヌゞャがどのように生き残るかに぀いおの悲しい話を聞きたした。

それでは、メむントピックに戻りたす。 ゞェネレヌタヌずプロミスの仕組みがわかったので、このような「サヌビス手順」を䜜成するこずは難しくありたせん。 サヌビスプロシヌゞャ自䜓は、玄束ずしお競争的に実行され、ゞェネレヌタをむンスタンス化しお維持し、 .then()コヌルバックを䜿甚しおメむンプロシヌゞャの最終結果に戻りたす。

次に、coプログラムに戻り、さらに詳しく説明したす。 co()は、ゞェネレヌタヌが同期倀でのみ動䜜できるように、スレヌブ劎働を匕き受けるサヌビス手順です。 すでにはるかに論理的に芋えたすよね

 co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }); 

スプリングボヌド関数に粟通しおいる人は、 co()をプロミスをキャストするスプリングボヌド関数の非同期バヌゞョンず考えるこずができたす。

プログラミングタスク-coは簡単

いいね co() , , . co()

  1. ,
  2. .next() , {done: false, value: [a Promise]}
  3. ( ), .next() ,
  4. , 4
  5. - {done: true, value: ...} , , co()

, co(), :



 function deferred(val) { return new Promise((resolve, reject) => resolve(val)); } co(function* asyncAdds(initialValue) { console.log(yield deferred(initialValue + 1)); console.log(yield deferred(initialValue + 2)); console.log(yield deferred(initialValue + 3)); }); function co(generator) { return new Promise((resolve, reject) => { //   }); } 

解決策

, ? - 10 co() , . , . ?

– co()

, , , , co() . , .throw() .



 function deferred(val) { return new Promise((resolve, reject) => resolve(val)); } function deferReject(e) { return new Promise((resolve, reject) => reject(e)); } co(function* asyncAdds() { console.log(yield deferred(1)); try { console.log(yield deferredError(new Error('To fail, or to not fail.'))); } catch (e) { console.log('To not fail!'); } console.log(yield deferred(3)); }); function co(generator) { return new Promise((resolve, reject) => { //   }); } 

解決策

. , , .next() onResolve() . onReject() , .throw() . try/catch , , try/catch .

, co() ! ! co() , , , . , ?

: async/await

co() . - , async/await? — ! , async await .

async , await , yield . await , async . async - .

, async/await , , - co() async yield await , * , .

 co(function* () { var user = yield fetchJson('/api/user/self'); var interests = yield fetchJson('/api/user/interests?userId=' + self.id); var recommendations = yield Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }); 

:

 async function () { var user = await fetchJson('/api/user/self'); var interests = await fetchJson('/api/user/interests?userId=' + self.id); var recommendations = await Promise.all( interests.map(i => fetchJson('/api/recommendations?topic=' + i))); render(user, interests, recommendations); }(); 

, :




Javascript , , « » co() , , , async/await . ? そうだね。

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


All Articles