JavaScript ES8および非同期/待機への移行

最近、私たちはES6の Material Promises:Patterns and Anti-Patternsを公​​開しました。 それに対するコメントで、読者は現代のJSプロジェクトで非同期コードを書く機能について議論しました。 ところで、私たちは彼らのコメントを読むことをお勧めします-あなたはそこに多くの興味深いものを見つけるでしょう。

画像

ユーザーilnuribatのアドバイスに基づいて、材料に投票を追加しました。その目的は、 promise 、callback、async / await構造の人気を調べることでした。 9月9日の時点で、約束と非同期/待機は投票の約43%を受け取り、非同期/待機のわずかなマージンで、コールバックは14%を獲得しました。 調査とコメントの結果を分析することで得られる主な結論は、利用可能なすべてのテクノロジーが重要であるということです。しかし、ますます多くのプログラマーが非同期/待機に引き寄せられています。 したがって、本日、async / awaitへの移行に関する記事の翻訳を公開することを決定しました。これは、約束に関する資料の続きです。

コールバック、約束、非同期/待機


先週、ES6に登場したJS機能であるpromise について書きました 。 約束は、 コールバック地獄から抜け出すための素晴らしい方法でした。 ただし、Node.js(バージョン7.6以降)でasync / awaitサポートが登場したため、一時的な即興の手段のようなものとして、promiseの認識を開発しました。 babelのようなトランスパイラーのおかげで、ブラウザーのコードでasync / awaitも使用できると言わなければなりません。

この記事では、 テンプレートリテラル矢印関数を含む最新のJS機能を使用します 。 ES6の革新のリストはこちらからご覧ください

非同期/待機が素晴らしいのはなぜですか?


最近まで、JavaScriptの非同期コードはせいぜい厄介に見えました。 Python、Ruby、Javaなどの言語からJavaScriptに切り替えた開発者にとって、コールバックとプロミスは、エラーが発生しやすく、プログラマを完全に混乱させる不当に複雑な構造であると思われました。

問題は、プログラマーにとって、同期ロジックと非同期ロジックの間に大きな違いがないことです。 プログラマーが非同期コードを書いているときに考える必要があるパフォーマンスと最適化に関する多くの問題がありますが、完全に異なる構文はすでに多すぎます。

同じロジックを実装する3つの例があります。 最初は通常の同期関数を使用し、2番目はコールバックを使用し、3番目はpromiseを使用します。 それぞれが同じ問題を解決します。HackerNewsで最も人気のある記事に関する情報をアップロードします。

同期バージョンの仮想的な例を次に示します。

 // :    ! let hn = require('@datafire/hacker_news').create(); let storyIDs = hn.getStories({storyType: 'top'}); let topStory = hn.getItem({itemID: storyIDs[0]}); console.log(`Top story: ${topStory.title} - ${topStory.url}`); 

ここではすべてが非常に簡単です-JSで書いた人にとって新しいことは何もありません。 コードには3つのステップがあります。材料識別子のリストを取得し、最も人気のあるものに関する情報をダウンロードし、結果を表示します。

これはすべて良いことですが、JavaScriptでイベントループをブロックすることはできません。 したがって、記事の識別子と人気のある記事に関する情報がファイルに由来する場合、データベースから読み取られる場合、またはリソースを大量に消費するI / O操作の結果としてプログラムにアクセスする場合、それらはネットワーク要求への応答という形で提供され、対応するコマンドは常に実行される必要がありますコールバックまたはプロミスを使用した非同期(これが、上記のコードが機能しない理由です。実際、 HackerNewsのクライアントはpromiseに 基づいています )。

コールバックに実装されているのと同じロジックを次に示します(例、これも仮想的なものです)。

 // :    ! let hn = require('@datafire/hacker_news').create(); hn.getStories({storyType: 'top'}, (err, storyIDs) => { if (err) throw err; hn.getItem({itemID: storyIDs[0]}, (err, topStory) => {   if (err) throw err;   console.log(`Top story: ${topStory.title} - ${topStory.url}`); }) }) 

うん。 これで、必要な機能を実装するコードフラグメントが相互に埋め込まれ、それらを水平方向に揃える必要があります。 3つではなく20のステップがある場合、後者を整列させるには40のスペースが必要になります! また、真ん中のどこかに新しいステップを追加する必要がある場合は、その下にあるすべてを再調整する必要があります。 これにより、Gitの異なるファイル状態の間に巨大で役に立たない違いが現れます。 また、この構造全体のすべてのステップでエラーを処理する必要があることに注意してください。 一連の操作を単一のtry / catchにグループ化することはできません。

ここで、promiseを使用して同じことを試してみましょう。

 let hn = require('@datafire/hacker_news').create(); Promise.resolve() .then(_ => hn.getStories({storyType: 'top'})) .then(storyIDs => hn.getItem({itemID: storyIDs[0])) .then(topStory => console.log(`Top story: ${topStory.title} - ${topStory.url}`)) 

だから、すでに良く見えます。 3つのステップはすべて水平に均等に配置され、中央に新しいステップを追加することは、新しい行を挿入することと同じくらい難しくありません。 その結果、 Promise.resolve()を使用する必要があるため、またここにあるすべての.then()構成のため、 Promise.resolve()の構文は少し冗長であるとPromise.resolve()ます。

通常の関数、コールバック、およびプロミスを理解したので、 async / awaitコンストラクトで同じことを行う方法を見てみましょう。

 let hn = require('@datafire/hacker_news').create(); (async () => { let storyIDs = await hn.getStories({storyType: 'top'}); let topStory = await hn.getItem({itemID: storyIDs[0]}); console.log(`Top story: ${topStory.title} - ${topStory.url}`); })(); 

これはすでにはるかに優れています! ここではawaitキーワードが使用されていることを除いて、同期コードとして取得されているようです。 さらに、 async使用して宣言された匿名関数にコードを配置したため、このコードは今後の作業に適しています。

メソッドhn.getStories()およびhn.getItem()は、 hn.getItem()を返すように設計されていると言わなければなりません。 実行されると、イベントループはブロックされません。 async / awaitおかげで、JS史上初めて、通常の宣言構文を使用して非同期コードを書くことができました!

非同期/待機に切り替える


それでは、プロジェクトでasync / awaitをどのように使い始めますか? すでにプロミスで作業している場合は、新しいテクノロジーに切り替える準備ができています。 promiseを返す関数は、 awaitキーワードを使用しawait呼び出すことができます。これにより、promiseを解決した結果が返されます。 ただし、コールバックからasync / awaitに切り替える場合は、最初にそれらをプロミスに変換する必要があります。

約束への非同期/待機への移行


約束を受け入れた開発者の最前線にいて、コードが.then()チェーンを使用して非同期ロジックを実装している場合、 async / await切り替えても問題は生じません。各.then() awaitを使用します。

さらに、 .catch()ブロックを標準のtry / catchブロックに置き換える必要があります。 ご覧のとおり、最後に同じアプローチを使用して、同期および非同期コンテキストでエラーを処理できます。

モジュールのトップレベルで awaitキーワードを使用できないことに注意することも重要です。 async宣言された関数内で使用する必要があります。

 let hn = require('@datafire/hacker_news').create(); //    : Promise.resolve() .then(_ => hn.getStories({storyType: 'top'})) .then(storyIDs => hn.getItem({itemID: storyIDs[0])) .then(topStory => console.log(topStory)) .catch(e => console.error(e)) //    async / await: (async () => { try {   let storyIDs = await hn.getStories({storyType: 'top'});   let topStory = await hn.getItem({itemID: storyIDs[0]});   console.log(topStory); }  catch (e) {   console.error(e); } })(); 

syncコールバックからの非同期/待機への移行


コードでまだコールバック関数を使用している場合、 async / awaitに切り替える最良の方法は、コールバックを事前に約束に変換することです。 次に、上記の手法を使用して、 async / awaitを使用するコードをasync / awaitを使用して書き換えます。 コールバックをプロミスに変換する方法については、 こちらをご覧ください

パターンと落とし穴


もちろん、新しい技術は常に新しい問題です。 コードをasync / awaitに変換するときに遭遇する可能性のある便利なテンプレートとよくある間違いを次に示します。

▍サイクル


私がJSで書き始めたときから、関数を引数として他の関数に渡すことは私のお気に入りの機能の1つです。 もちろん、コールバックは混乱していますが、私にとっては、通常のforループの代わりにArray.forEachを使用することをArray.forEachます。

 const BEATLES = ['john', 'paul', 'george', 'ringo']; //   for: for (let i = 0; i < BEATLES.length; ++i) { console.log(BEATLES[i]); } //  Array.forEach: BEATLES.forEach(beatle => console.log(beatle)) 

ただし、 awaitを使用する場合、 Array.forEachメソッドは同期操作を実行するように設計されているため、正しく機能しません。

 let hn = require('@datafire/hacker_news').create(); (async () => { let storyIDs = await hn.getStories({storyType: 'top'}); storyIDs.forEach(async itemID => {   let details = await hn.getItem({itemID});   console.log(details); }); console.log('done!'); // !      ,    getItem()  . })(); 

この例では、 forEachgetItem()非同期非同期呼び出しをgetItem()開始し、結果を期待せずにすぐに制御を返します。したがって、画面に最初に表示されるのは「完了!」行です。

非同期操作の結果を待つ必要がある場合、これは通常のforループ(操作を順次実行する)またはPromise.allコンストラクト(操作を並列に実行する)のいずれかが必要であることを意味します。

 let hn = require('@datafire/hacker_news').create(); (async () => { let storyIDs = await hn.getStories({storyType: 'top'}); //   for (  ) for (let i = 0; i < storyIDs.length; ++i) {   let details = await hn.getItem({itemID: storyIDs[i]});   console.log(details); } //  Promise.all (  ) let detailSet = await Promise.all(storyIDs.map(itemID => hn.getItem({itemID}))); detailSet.forEach(console.log); })(); 

▍最適化


async / awaitを使用すると、非同期コードを記述していることを考える必要がなくなります。 これは素晴らしいことですが、ここに新しい技術の最も危険なtrapがあります。 実際、このアプローチでは、パフォーマンスに大きな影響を与える可能性のある些細なことを忘れることができます。

例を考えてみましょう。 2人のHacker Newsユーザーに関する情報を取得して、カルマを比較したいとします。 通常の実装は次のとおりです。

 let hn = require('@datafire/hacker_news').create(); (async () => { let user1 = await hn.getUser({username: 'sama'}); let user2 = await hn.getUser({username: 'pg'}); let [more, less] = [user1, user2].sort((a, b) => b.karma - a.karma); console.log(`${more.id} has more karma (${more.karma}) than ${less.id} (${less.karma})`); })(); 

コードは非常に機能していますが、 getUser() 2番目の呼び出しは、最初の呼び出しが完了するまで実行されません。 呼び出しは独立しており、並行して実行できます。 したがって、次の方がより良い解決策です。

 let hn = require('@datafire/hacker_news').create(); (async () => { let users = await Promise.all([   hn.getUser({username: 'sama'}),   hn.getUser({username: 'pg'}), ]); let [more, less] = users.sort((a, b) => b.karma - a.karma); console.log(`${more.id} has more karma (${more.karma}) than ${less.id} (${less.karma})`); })(); 

この方法を使用する前に、コマンドの並列実行によって目的を達成できることを確認する価値があることに注意してください。 多くの場合、非同期操作は順番に実行する必要があります。

まとめ


非同期JavaScriptコードの開発でasync / awaitコンストラクトが導入した驚くべき革新を紹介できたことを願っasync / awaitます。 同期コンストラクトと同じ構文を使用して非同期コンストラクトを記述する機能は、現代のプログラミングの標準です。 JavaScriptで同じ機会が利用できるようになったという事実は、この言語で書いているすべての人にとって大きな前進です。

親愛なる読者! 以前の出版物の調査から、多くの人が非同期/待機を使用していることがわかりました。 したがって、あなたの経験を共有してください。

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


All Articles