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



. ? そうだね。



All Articles