著者は、オープンインターネット/フリースピーチ基金を寄付の受取先として選びました、それは寄付のための執筆プログラムの一環です。
はじめに
JavaScriptの多くのプログラムでは、コードは開発者が書くときに行ごとに実行されます。これは同期実行と呼ばれ、行は書かれた順序で一つずつ実行されます。しかし、コンピューターに与える命令がすぐに処理される必要はありません。例えば、ネットワークリクエストを送信すると、コードを実行しているプロセスはデータが戻ってくるのを待たなければなりません。この場合、ネットワークリクエストが完了するのを待っている間に他のコードを実行しないと時間が無駄になります。この問題を解決するために、開発者は非同期プログラミングを使用します。非同期プログラミングでは、コードの行が書かれた順序とは異なる順序で実行されます。非同期プログラミングを使用すると、ネットワークリクエストなどの長時間かかるアクティビティの完了を待ちながら他のコードを実行できます。
JavaScriptのコードは、コンピュータープロセス内の単一スレッドで実行されます。そのコードはこのスレッドで同期的に処理され、1度に1つの命令のみが実行されます。したがって、このスレッドで長時間実行されるタスクを行う場合、そのタスクが完了するまで、残りのコードがすべてブロックされます。JavaScriptの非同期プログラミング機能を活用することで、この問題を回避するために長時間実行されるタスクをバックグラウンドスレッドにオフロードできます。タスクが完了すると、タスクのデータを処理するために必要なコードがメインの単一スレッドに戻されます。
このチュートリアルでは、JavaScriptが非同期タスクをどのように管理するかを、イベントループの助けを借りて学びます。これは、別のタスクを待っている間に新しいタスクを完了するJavaScriptの構築物です。その後、非同期プログラミングを使用して、スタジオジブリAPIから映画のリストをリクエストし、データをCSVファイルに保存するプログラムを作成します。非同期コードは、コールバック、プロミス、およびasync
/await
キーワードを使用して3つの方法で記述されます。
注意: この記事の執筆時点では、非同期プログラミングはもはやコールバックのみを使用して行われていませんが、この時代遅れの方法を学ぶことは、JavaScriptコミュニティが今ではプロミスを使用する理由を理解するための大きな文脈を提供できます。async
/await
キーワードを使用すると、プロミスをより簡潔に使用できるため、このキーワードはこの記事の執筆時点ではJavaScriptで非同期プログラミングを行う標準的な方法です。
前提条件
- 開発マシンにNode.jsがインストールされていること。このチュートリアルではバージョン10.17.0を使用しています。macOSまたはUbuntu 18.04にこれをインストールする方法については、macOSでNode.jsをインストールしてローカル開発環境を作成する方法またはUbuntu 18.04にNode.jsをインストールする方法のPPAを使用してインストールするセクションの手順に従ってください。
- プロジェクトでパッケージをインストールする方法に慣れている必要があります。npmとpackage.jsonを使用したNode.jsモジュールの使用方法については、私たちのガイドを読んでください。Node.jsモジュールをnpmとpackage.jsonと一緒に使用する方法。
- 非同期にそれらを使用する方法を学ぶ前に、JavaScriptで関数を作成して実行することに慣れていることが重要です。導入または復習が必要な場合は、私たちのガイドを読んでください。JavaScriptで関数を定義する方法
イベントループ
イベントループ
まず、JavaScriptの関数実行の内部動作について学びましょう。これがどのように動作するかを理解することで、非同期コードをより意図的に書くことができるようになり、将来のコードのトラブルシューティングに役立ちます。
JavaScriptインタプリタがコードを実行するとき、呼び出されたすべての関数はJavaScriptのコールスタックに追加されます。コールスタックはスタックであり、アイテムはトップにのみ追加でき、トップから削除できます。スタックは「最後に入れたものが最初に出てくる」またはLIFOの原則に従います。スタックに2つのアイテムを追加すると、最も最近追加されたアイテムが最初に削除されます。
コールスタックを使用した例を示しましょう。JavaScriptが関数functionA()
の呼び出しを検出すると、それがコールスタックに追加されます。その関数functionA()
が別の関数functionB()
を呼び出すと、functionB()
がスタックの一番上に追加されます。JavaScriptが関数の実行を完了すると、それがコールスタックから削除されます。したがって、JavaScriptは最初にfunctionB()
を実行し、完了したらスタックから削除して、functionA()
の実行を完了し、コールスタックから削除します。これが、内部関数が常に外部関数よりも先に実行される理由です。
JavaScriptが非同期の操作、例えばファイルへの書き込みなどに遭遇すると、それをメモリ内のテーブルに追加します。このテーブルには、操作、完了条件、および完了時に呼び出される関数が格納されます。操作が完了すると、JavaScriptは関連する関数をメッセージキューに追加します。キューは、アイテムを下部にのみ追加でき、上部からのみ削除できる別のリスト構造です。メッセージキューでは、2つ以上の非同期操作がそれぞれの関数の実行を待っている場合、最初に完了した非同期操作の関数が最初に実行されるようにマークされます。
メッセージキュー内の関数は、呼び出しスタックに追加されるのを待っています。イベントループは、呼び出しスタックが空かどうかを常に確認するプロセスです。空であれば、メッセージキューの最初のアイテムが呼び出しスタックに移動します。JavaScriptは、コードで解釈される関数呼び出しよりも、メッセージキュー内の関数を優先します。呼び出しスタック、メッセージキュー、およびイベントループの組み合わせ効果により、JavaScriptコードは非同期のアクティビティを管理しながら処理されます。
イベントループの高レベルな理解を持った今、書いた非同期コードがどのように実行されるかがわかりました。この知識を活用して、コールバック、プロミス、およびasync
/await
の3つの異なるアプローチで非同期コードを作成できます。
コールバックを使用した非同期プログラミング
A callback function is one that is passed as an argument to another function, and then executed when the other function is finished. We use callbacks to ensure that code is executed only after an asynchronous operation is completed.
長い間、コールバックは非同期コードを書くための最も一般的なメカニズムでしたが、今ではほとんど使われなくなりました。なぜなら、コードを読みにくくする可能性があるからです。このステップでは、コールバックを使用した非同期コードの例を書いて、他の戦略の効率向上を確認するための基準として使用します。
他の関数でコールバック関数を使用する方法はたくさんあります。一般的に、それらはこの構造を取ります:
JavaScriptやNode.jsで構文的に必要とされているわけではありませんが、コールバックを簡単に特定できるようにするために、外部関数の最後の引数としてコールバック関数を配置することが一般的な慣習です。JavaScript開発者がコールバックとして無名関数を使用することも一般的です。無名関数とは、名前を持たずに作成される関数のことです。関数が引数リストの最後に定義されている場合、通常は読みやすくなります。
コールバックを示すために、Studio Ghibliの映画のリストをファイルに書き込むNode.jsモジュールを作成しましょう。まず、JavaScriptファイルとその出力を格納するフォルダを作成します:
その後、そのフォルダに移動します:
Studio Ghibli APIにHTTPリクエストを行い、その結果をコールバック関数でログに記録します。これには、HTTPレスポンスのデータにアクセスできるライブラリをインストールします。
ターミナルで、npmを初期化して後でパッケージを参照できるようにします:request
それから、request
ライブラリをインストールします:
新しいファイルをcallbackMovies.js
としてテキストエディター(例: nano
)で開きます:
テキストエディターで、以下のコードを入力します。まずは、request
モジュールを使用してHTTPリクエストを送信することから始めましょう:
最初の行で、npm経由でインストールされたrequest
モジュールをロードします。モジュールはHTTPリクエストを行うことができる関数を返し、その関数をrequest
定数に保存します。
request()
関数を使用してHTTPリクエストを行います。次に、ハイライトされた変更を追加して、HTTPリクエストからのデータをコンソールに出力しましょう:
request()
関数を使用する際、2つのパラメーターを渡します:
- リクエストを行おうとしているウェブサイトのURL
- A callback function that handles any errors or successful responses after the request is complete
コールバック関数には、error
、response
、body
という3つの引数があります。HTTPリクエストが完了すると、引数は結果に応じて自動的に値が設定されます。リクエストが送信に失敗した場合、error
にはオブジェクトが含まれますが、response
とbody
はnull
になります。リクエストが正常に送信された場合、HTTPレスポンスはresponse
に保存されます。HTTPレスポンスがデータ(この例ではJSONを取得)を返す場合、データはbody
に設定されます。
私たちのコールバック関数はまず、エラーが発生したかどうかをチェックします。コールバック内でまずエラーをチェックすることはベストプラクティスです。これにより、コールバックの実行がデータが欠落した状態で続行されることがありません。この場合、エラーと関数の実行をログに記録します。次に、レスポンスのステータスコードを確認します。サーバーが常に利用可能でない場合や、APIが変更されることにより以前は正常だったリクエストが不正確になることがあります。ステータスコードが200
であることを確認することにより、「OK」というリクエストが送信されたことを確認し、期待されるレスポンスであることに自信を持つことができます。
最後に、レスポンスボディをArray
に解析し、各映画をループしてその名前と公開年をログに記録します。
ファイルを保存して終了したら、次のスクリプトを実行します:
以下の出力が得られます:
OutputCastle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014
スタジオジブリの映画のリストと公開年を受け取りました。現在ログに記録している映画リストをファイルに書き込んで、このプログラムを完成させましょう。
次に、テキストエディターでcallbackMovies.js
ファイルを更新し、以下のハイライトされたコードを含めて、映画データを含むCSVファイルを作成します:
ハイライトされた変更を注意深く見ると、fs
モジュールをインポートしていることがわかります。このモジュールはすべてのNode.jsインストールに標準で含まれており、ファイルに非同期で書き込むwriteFile()
メソッドを含んでいます。
movieList
にデータをログ出力する代わりに、それを文字列変数に追加します。次に、writeFile()
を使用してmovieList
の内容を新しいファイルであるcallbackMovies.csv
に保存します。最後に、writeFile()
関数にコールバックを提供します。この関数は1つの引数error
を持ちます。これにより、ファイルに書き込みできない場合などのケースを処理できます。たとえば、node
プロセスを実行しているユーザーにその権限がない場合などです。
ファイルを保存して、このNode.jsプログラムを再度次のように実行します:
ghibliMovies
フォルダー内に、次の内容を持つcallbackMovies.csv
が表示されます:
Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014
重要な点として、CSVファイルへの書き込みをHTTPリクエストのコールバック内で行うことです。コードがコールバック関数内にあると、HTTPリクエストが完了した後にのみファイルに書き込まれます。CSVファイルを書き込んだ後にデータベースに通信する場合は、writeFile()
のコールバック内で呼び出される別の非同期関数を作成します。非同期コードが増えるほど、ネストされるコールバック関数が増えます。
5つの非同期操作を実行し、それぞれが他の操作が完了したときにのみ実行されるようにしたいとします。これをコード化する場合、次のようになります:
ネストされたコールバックに多くのコードがあると、それらはかなり複雑で読みづらくなります。JavaScriptプロジェクトが規模と複雑さを増すにつれて、この効果はより顕著になり、最終的には管理しきれなくなります。そのため、開発者は非同期操作を処理するためにコールバックを使用しなくなりました。非同期コードの構文を改善するために、Promisesを使用することができます。非同期プログラミングのためのPromisesを使用する
約束事
A promise is a JavaScript object that will return a value at some point in the future. Asynchronous functions can return promise objects instead of concrete values. If we get a value in the future, we say that the promise was fulfilled. If we get an error in the future, we say that the promise was rejected. Otherwise, the promise is still being worked on in a pending state.
通常、Promisesは次の形式を取ります:
このテンプレートに示されているように、Promisesもコールバック関数を使用します。約束が果たされたときに実行されるthen()
メソッドのためのコールバック関数があります。また、約束が実行されている間に発生したエラーを処理するためのcatch()
メソッドのコールバック関数もあります。
プロミスを使用して、Studio Ghibliプログラムをプロミスを使用するように書き直して、実際に手を動かしてみましょう。
AxiosはJavaScript用のプロミスベースのHTTPクライアントなので、さっそくインストールしましょう:
好きなテキストエディタで、新しいファイルpromiseMovies.js
を作成します:
プログラムはaxios
を使用してHTTPリクエストを行い、次に特別なPromiseベースのfs
を使用して新しいCSVファイルに保存します。
promiseMovies.js
にこのコードを入力して、Axiosをロードし、映画APIにHTTPリクエストを送信できるようにします:
最初の行で、axios
モジュールをロードし、返された関数をaxios
という定数に格納します。その後、axios.get()
メソッドを使用してAPIにHTTPリクエストを送信します。
axios.get()
メソッドはPromiseを返します。このPromiseをチェーンして、ジブリの映画のリストをコンソールに印刷します:
何が起こっているかを見てみましょう。axios.get()
でHTTP GETリクエストを行った後、Promiseが達成された時だけ実行されるthen()
関数を使用します。この場合、コールバックの例と同様に映画を画面に表示します。
このプログラムを改善するために、HTTPデータをファイルに書き込むために、ハイライトされたコードを追加します:
さらに、fs
モジュールを再度インポートします。 fs
のインポートの後に.promises
があることに注意してください。Node.jsには、コールバックベースのfs
ライブラリのPromiseベースのバージョンが含まれているため、レガシープロジェクトで後方互換性が壊れないようになっています。
HTTPリクエストを処理する最初のthen()
関数は、今やコンソールに出力する代わりにfs.writeFile()
を呼び出します。Promiseベースのfs
のバージョンをインポートしたので、writeFile()
関数もまた別のPromiseを返します。そのため、writeFile()
のPromiseが達成されたときにもう1つのthen()
関数を追加します。
A promise can return a new promise, allowing us to execute promises one after the other. This paves the way for us to perform multiple asynchronous operations. This is called promise chaining, and it is analogous to nesting callbacks. The second then()
is only called after we successfully write to the file.
注意: この例では、コールバックの例と同様にHTTPステータスコードをチェックしていませんでした。デフォルトでは、axios
はエラーを示すステータスコードを受け取った場合、約束を果たしません。したがって、これを検証する必要はもはやありません。
このプログラムを完成させるには、以下で強調されているように約束にcatch()
関数をチェーンさせてください:
約束のチェーンの中でどれかが果たされない場合、JavaScriptは自動的に定義されていればcatch()
関数に移動します。そのため、非同期操作が2つあるにもかかわらず、catch()
句が1つだけある理由です。
次を実行してプログラムが同じ出力を生成することを確認しましょう:
ghibliMovies
フォルダーには、promiseMovies.csv
ファイルが含まれています:
Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014
約束を使用すると、コールバックのみを使用するよりもはるかに簡潔なコードを書くことができます。コールバックの約束チェーンは、コールバックのネストよりもクリーンなオプションです。ただし、より多くの非同期呼び出しを行うと、約束チェーンが長くなり、保守が難しくなります。
コールバックと約束の冗長性は、非同期タスクの結果を持つ必要があるときに関数を作成する必要があるために生じます。より良い経験は、非同期の結果を待って関数の外に変数に入れることです。その方法で、変数の結果を関数なしで使用できます。これはasync
とawait
キーワードを使用して達成できます。
JavaScriptをasync
/await
で書く
async
/await
キーワードは、プロミスを扱う際の代替構文を提供します。プロミスの結果をthen()
メソッドで利用する代わりに、結果は他の関数と同様に値として返されます。JavaScriptには、非同期関数であることを示すためにasync
キーワードを使用し、プロミスの結果を返すようにJavaScriptに指示するためにawait
キーワードを使用します。
一般的に、async
/await
の使用は次のようになります:
async
/await
を使用すると、Studio Ghibliプログラムを改善できる方法を見てみましょう。テキストエディタを使用して、新しいファイルasyncAwaitMovies.js
を作成して開きます:
新しく開いたJavaScriptファイルには、プロミスの例で使用したのと同じモジュールをインポートして開始します:
インポートはpromiseMovies.js
と同じです。なぜなら、async
/await
はプロミスを使用するからです。
今度は、非同期コードを持つ関数を作成するためにasync
キーワードを使用します:
新しい関数saveMovies()
を作成しますが、その定義の先頭にasync
を含めます。これは重要です。なぜなら、非同期関数内でしかawait
キーワードを使用できないからです。
次のHTTPリクエストを作成して、ジブリAPIから映画のリストを取得するためにawait
キーワードを使用します:
saveMovies()
関数では、以前と同様にaxios.get()
を使用してHTTPリクエストを行います。今回はthen()
関数をチェーンしません。代わりに、その前にawait
を追加します。JavaScriptがawait
を見つけると、axios.get()
の実行が完了しresponse
変数が設定されるまで、関数の残りのコードを実行しません。他のコードは映画データを保存し、ファイルに書き込むためのものです。
映画データをファイルに書き込みましょう:
fs.writeFile()
でファイルに書き込む際にもawait
キーワードを使用します。
この関数を完成させるために、約束がスローするエラーをキャッチする必要があります。これを行うには、コードをtry
/catch
ブロックでカプセル化します:
約束が失敗する可能性があるため、非同期コードをtry
/catch
節で囲みます。これにより、HTTPリクエストまたはファイル書き込み操作が失敗した場合にスローされるエラーがキャプチャされます。
最後に、非同期関数saveMovies()
を呼び出して、プログラムをnode
で実行した際に実行されるようにします。
一目見ると、これは典型的な同期JavaScriptコードブロックのように見えます。関数のやりとりが少なく、少し整然として見えます。これらの小さな調整により、async
/await
を使用した非同期コードがよりメンテナンスしやすくなります。
プログラムのこのイテレーションをテストするには、ターミナルにこれを入力してください:
ghibliMovies
フォルダーに、次の内容の新しいasyncAwaitMovies.csv
ファイルが作成されます:
Castle in the Sky, 1986
Grave of the Fireflies, 1988
My Neighbor Totoro, 1988
Kiki's Delivery Service, 1989
Only Yesterday, 1991
Porco Rosso, 1992
Pom Poko, 1994
Whisper of the Heart, 1995
Princess Mononoke, 1997
My Neighbors the Yamadas, 1999
Spirited Away, 2001
The Cat Returns, 2002
Howl's Moving Castle, 2004
Tales from Earthsea, 2006
Ponyo, 2008
Arrietty, 2010
From Up on Poppy Hill, 2011
The Wind Rises, 2013
The Tale of the Princess Kaguya, 2013
When Marnie Was There, 2014
これで、JavaScriptのasync
/await
機能を使用して非同期コードを管理しました。
結論
このチュートリアルでは、JavaScriptが関数を実行し、イベントループで非同期操作を管理する方法を学びました。次に、さまざまな非同期プログラミング技術を使用して、映画データのHTTPリクエストを行った後にCSVファイルを作成するプログラムを作成しました。最初に、廃止されたコールバックベースのアプローチを使用しました。その後、プロミスを使用し、最後にasync
/await
を使用してプロミスの構文をより簡潔にしました。
Node.jsを使用した非同期コードの理解により、API呼び出しに依存するプログラムなど、非同期プログラミングを活用するプログラムを開発できるようになりました。次のリストをご覧ください。公共APIを使用するには、このチュートリアルで行ったように非同期のHTTPリクエストを行う必要があります。さらなる学習のためには、ここで学んだテクニックを実践するためにこれらのAPIを使用するアプリを構築してみてください。
Source:
https://www.digitalocean.com/community/tutorials/how-to-write-asynchronous-code-in-node-js