Node.js の動作のしくみ
このユニットでは、Node.js が JavaScript ランタイムへ入ってくるタスクをどのように処理するかについて説明します。
タスクの種類
JavaScript アプリケーションには、以下の 2 種類のタスクがあります。
- 同期タスク:これらのタスクは順番に実行されます。 完了のために別のリソースには依存しません。 例としては、数学演算や文字列操作が挙げられます。
- 非同期:これらのタスクは、他のリソースに依存しているため、すぐには完了しない可能性があります。 例としては、ネットワーク要求やファイル システム操作が挙げられます。
プログラムはできるだけ速く実行したいため、JavaScript エンジンが非同期操作からの応答を待機している間も動作を継続できるようにする必要があります。 これを行うために、非同期タスクはタスク キューに追加され、次のタスクの実行が継続されます。
イベント ループを使用してタスク キューを管理する
Node.js では、JavaScript エンジンのイベント ドリブン アーキテクチャを使用して非同期要求を処理します。 次の図は、V8 イベント ループのしくみを大まかに示しています。
適切な構文 (以下に示す) で示される非同期タスクが、イベント ループに追加されます。 タスクには、実行する作業と、結果を受け取るコールバック関数が含まれます。 集中的な操作が完了すると、コールバック関数が結果でトリガーされます。
同期操作と非同期操作
Node.js API では、ファイル操作などの一部の同じ操作に対して非同期と同期の両方の操作が提供されます。 一般には、常に非同期優先と考える必要がありますが、同期操作を使用する場合もあります。
たとえば、コマンド ライン インターフェイス (CLI) でファイルが読み取られてから、そのファイル内のデータがすぐに使用される場合です。 この場合は、同期バージョンのファイル操作を使用できます。これは、アプリケーションの使用を待機している他のシステムやユーザーが存在しないためです。
ただし、Web サーバーを構築する場合は、単一スレッドで他のユーザー要求を処理できなくならないように、非同期バージョンのファイル操作を常に使用する必要があります。
TailWind Traders の開発者としての作業時に、同期と非同期の操作の違いと、それぞれをいつ使用するかを理解する必要があります。
非同期操作によるパフォーマンス
Node.js は、サーバー タスクの作成を高速かつ高パフォーマンスにする JavaScript の独自のイベントドリブンの性質を利用します。 JavaScript で非同期手法を ''正しく'' 使用すると、V8 エンジンによって可能になったパフォーマンスの向上により、C のような低レベル言語と同じパフォーマンス結果を得ることができます。
非同期手法には 3 つのスタイルがあり、作業時に認識できる必要があります。
- async/await (推奨):
async
とawait
キーワードを使用して非同期操作の結果を受け取る最新の非同期手法。 async/await は、多くのプログラミング言語で使用されます。 一般に、新しい依存関係を持つ新しいプロジェクトでは、このスタイルの非同期コードが使用されます。 - コールバック:コールバック関数を使用して非同期操作の結果を受け取る元の非同期手法。 これは、以前のコード ベースと以前の Node.js API で確認できます。
- Promise:Promise オブジェクトを使用して非同期操作の結果を受け取る新しい非同期手法。 これは、新しいコード ベースと新しい Node.js API で確認できます。 更新されない以前の API をラップするために、作業時に Promise ベースのコードを記述する必要がある場合があります。 このラップに Promise を使うことで、新しい async/await スタイルのコードよりも広い範囲の Node.js バージョン管理されたプロジェクトでコードを使用できるようになります。
async/await
async/await は、非同期プログラミングを処理するための最新の方法です。 async/await は Promise の上の構文糖であり、非同期コードが同期コードのように見えるようにします。 また、読みやすく保守も容易です。
同じ例で async/await を使用すると次のようになります。
// async/await asynchronous example
const fs = require('fs').promises;
const filePath = './file.txt';
// `async` before the parent function
async function readFileAsync() {
try {
// `await` before the async method
const data = await fs.readFile(filePath, 'utf-8');
console.log(data);
console.log('Done!');
} catch (error) {
console.log('An error occurred...: ', error);
}
}
readFileAsync()
.then(() => {
console.log('Success!');
})
.catch((error) => {
console.log('An error occurred...: ', error);
});
ES2017 で async/await がリリースされたとき、キーワードは最上位の関数が Promise である関数でのみ使用できました。 promise には then
セクションと catch
セクションを含める必要はありませんでしたが、実行するには promise
構文が必要でした。
async
関数は、内部に await
呼び出しがない場合でも、常に promise を返します。 promise は、関数から返された値を使って解決されます。 関数からエラーがスローされた場合、promise はスローされた値で拒否されます。
Promise
入れ子になったコールバックは読み取りと管理が困難になる可能性があるため、Node.js では Promise のサポートが追加されました。 Promise は、非同期操作の最終的な完了 (または失敗) を表すオブジェクトです。
Promise 関数の形式は次のとおりです。
// Create a basic promise function
function promiseFunction() {
return new Promise((resolve, reject) => {
// do something
if (error) {
// indicate success
reject(error);
} else {
// indicate error
resolve(data);
}
});
}
// Call a basic promise function
promiseFunction()
.then((data) => {
// handle success
})
.catch((error) => {
// handle error
});
then
メソッドは Promise が履行された場合に呼び出され、catch
メソッドは Promise が拒否された場合に呼び出されます。
Promise を使用してファイルを非同期的に読み取る場合、コードは次のようになります。
// promises asynchronous example
const fs = require('fs').promises;
const filePath = './file.txt';
// request to read a file
fs.readFile(filePath, 'utf-8')
.then((data) => {
console.log(data);
console.log('Done!');
})
.catch((error) => {
console.log('An error occurred...: ', error);
});
console.log(`I'm the last line of the file!`);
最上位の async/await
Node.js の最新バージョンでは、ES6 モジュールの最上位の async/await が追加されました。 この機能を使用するには、package.json に type
という名前のプロパティを module
の値で追加する必要があります。
{
"type": "module"
}
これにより、コードの最上位レベルで await
キーワードを使用できます。
// top-level async/await asynchronous example
const fs = require('fs').promises;
const filePath = './file.txt';
// `async` before the parent function
try {
// `await` before the async method
const data = await fs.readFile(filePath, 'utf-8');
console.log(data);
console.log('Done!');
} catch (error) {
console.log('An error occurred...: ', error);
}
console.log("I'm the last line of the file!");
コールバック
Node.js が最初にリリースされた頃には、非同期プログラミングはコールバック関数を使用することで処理されていました。 コールバックは、他の関数に引数として渡される関数です。 タスクが完了すると、コールバック関数が呼び出されます。
関数のパラメーターの順序は重要です。 コールバック関数は、関数の最後のパラメーターです。
// Callback function is the last parameter
function(param1, param2, paramN, callback)
保持するコード内の関数名は、callback
と呼ばれない可能性があります。 cb
、done
、または next
と呼ばれる場合があります。 関数の名前は重要ではありませんが、パラメーターの順序は重要です。
関数が非同期であることを示す構文がないことに注目してください。 ドキュメントを読んだり、コードに目を通したりして、関数が非同期であることを知る必要があります。
名前付きコールバック関数を使用したコールバックの例
次のコードでは、非同期関数をコールバックから分離させます。 これは読みやすく理解しやすく、他の非同期関数のコールバックを再利用できます。
// callback asynchronous example
// file system module from Node.js
const fs = require('fs');
// relative path to file
const filePath = './file.txt';
// callback
const callback = (error, data) => {
if (error) {
console.log('An error occurred...: ', error);
} else {
console.log(data); // Hi, developers!
console.log('Done!');
}
};
// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function
fs.readFile(filePath, 'utf-8', callback);
console.log("I'm the last line of the file!");
正しい結果は次のとおりです。
I'm the last line of the file!
Hi, developers!
Done!
まず、非同期関数 fs.readFile
が開始され、イベント ループに入ります。 その後、最後の console.log
である次のコード行にコードの実行が続行されます。 ファイルが読み取られた後、コールバック関数が呼び出され、2 つの console.log ステートメントが実行されます。
匿名関数を使用したコールバックの例
次の例では、匿名コールバック関数を使用します。つまり、関数には名前がなく、他の匿名関数では再利用できません。
// callback asynchronous example
// file system module from Node.js
const fs = require('fs');
// relative path to file
const filePath = './file.txt';
// async request to read a file
//
// parameter 1: filePath
// parameter 2: encoding of utf-8
// parmeter 3: callback function () => {}
fs.readFile(filePath, 'utf-8', (error, data) => {
if (error) {
console.log('An error occurred...: ', error);
} else {
console.log(data); // Hi, developers!
console.log('Done!');
}
});
console.log("I'm the last line of the file!");
正しい結果は次のとおりです。
I'm the last line of the file!
Hi, developers!
Done!
コードが実行されると、非同期関数 fs.readFile
が開始され、イベント ループに入ります。 次に、最後の console.log
である次のコード行に実行が続行されます。 ファイルが読み取られると、コールバック関数が呼び出され、2 つの console.log ステートメントが実行されます。
入れ子になったコールバック
後続の非同期コールバックを呼び出してから別のものを呼び出す必要がある場合があるため、コールバック コードが入れ子になる可能性があります。 これはコールバック地獄と呼ばれ、読み取りと保守が困難です。
// nested callback example
// file system module from Node.js
const fs = require('fs');
fs.readFile(param1, param2, (error, data) => {
if (!error) {
fs.writeFile(paramsWrite, (error, data) => {
if (!error) {
fs.readFile(paramsRead, (error, data) => {
if (!error) {
// do something
}
});
}
});
}
});
同期 API
Node.js には、同期 API のセットもあります。 これらの API は、タスクが完了するまでプログラムの実行をブロックします。 同期 API は、ファイルを読み取った後に、ファイル内のデータをすぐに使用する場合に便利です。
Node.js の同期 (ブロッキング) 関数では、functionSync
の名前付け規則が使用されます。 たとえば、非同期 readFile
API には readFileSync
という名前の同期 API が存在します。 コードを読みやすく理解しやすいものにするために、独自のプロジェクトでもこの標準を守ることが重要です。
// synchronous example
const fs = require('fs');
const filePath = './file.txt';
try {
// request to read a file
const data = fs.readFileSync(filePath, 'utf-8');
console.log(data);
console.log('Done!');
} catch (error) {
console.log('An error occurred...: ', error);
}
TailWind Traders の新しい開発者として、任意の種類の Node.js コードを変更するように求められる場合があります。 同期と非同期 の API の違いと、非同期コードのさまざまな構文を理解することが重要です。