非同期フロー制御
この記事の内容は、Mixu's Node.js Bookに大きく影響を受けています。
JavaScriptは、その中心において、「メイン」スレッドでノンブロッキングとなるように設計されています。これは、ビューがレンダリングされる場所です。ブラウザでこの重要性を想像できるでしょう。メインスレッドがブロックされると、エンドユーザーが恐れる悪名高い「フリーズ」が発生し、他のイベントをディスパッチできなくなり、データ取得の損失などが発生します。
これは、関数型プログラミングスタイルのみが解決できる独自の制約を生み出します。ここでコールバックが登場します。
ただし、コールバックは、より複雑な手順では処理が難しくなる可能性があります。これは多くの場合、「コールバック地獄」をもたらします。コールバックを持つ複数のネストされた関数が、コードの読み取り、デバッグ、整理などをより困難にします。
async1(function (input, result1) {
async2(function (result2) {
async3(function (result3) {
async4(function (result4) {
async5(function (output) {
// do something with output
});
});
});
});
});
もちろん、現実には、`result1`、`result2`などを処理するための追加のコード行がほとんどの場合存在するため、この問題の長さと複雑さは、通常、上記の例よりもはるかに厄介なコードになります。
ここで*関数*が非常に役立ちます。より複雑な操作は、多くの関数で構成されています
- イニシエータースタイル/入力
- ミドルウェア
- ターミネーター
「イニシエータースタイル/入力」は、シーケンスの最初の関数です。この関数は、操作の元の入力を(存在する場合)受け入れます。操作は、実行可能な一連の関数であり、元の入力は主に
- グローバル環境の変数
- 引数あり、または引数なしの直接呼び出し
- ファイルシステムまたはネットワークリクエストによって取得された値
ネットワークリクエストは、外部ネットワーク、同じネットワーク上の別のアプリケーション、または同じネットワークまたは外部ネットワーク上のアプリ自体によって開始される着信リクエストです。
ミドルウェア関数は別の関数を返し、ターミネーター関数はコールバックを呼び出します。以下は、ネットワークまたはファイルシステムリクエストへのフローを示しています。ここでは、これらの値はすべてメモリ内で使用できるため、レイテンシは0です。
function final(someInput, callback) {
callback(`${someInput} and terminated by executing callback `);
}
function middleware(someInput, callback) {
return final(`${someInput} touched by middleware `, callback);
}
function initiate() {
const someInput = 'hello this is a function ';
middleware(someInput, function (result) {
console.log(result);
// requires callback to `return` result
});
}
initiate();
状態管理
関数は状態依存する場合としない場合があります。関数の入力または他の変数が外部関数に依存する場合、状態依存が発生します。
このように、状態管理には主に2つの戦略があります
- 変数を関数に直接渡すこと、および
- キャッシュ、セッション、ファイル、データベース、ネットワーク、またはその他の外部ソースから変数値を取得すること。
グローバル変数については触れていません。グローバル変数で状態を管理することは、多くの場合、状態を保証するのが難しく、または不可能にするずさんなアンチパターンです。複雑なプログラムでは、可能な限りグローバル変数を避ける必要があります。
制御フロー
オブジェクトがメモリ内で使用できる場合、反復が可能であり、制御フローは変更されません
function getSong() {
let _song = '';
let i = 100;
for (i; i > 0; i -= 1) {
_song += `${i} beers on the wall, you take one down and pass it around, ${
i - 1
} bottles of beer on the wall\n`;
if (i === 1) {
_song += "Hey let's get some more beer";
}
}
return _song;
}
function singSong(_song) {
if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
console.log(_song);
}
const song = getSong();
// this will work
singSong(song);
ただし、データがメモリの外部に存在する場合、反復は機能しなくなります
function getSong() {
let _song = '';
let i = 100;
for (i; i > 0; i -= 1) {
/* eslint-disable no-loop-func */
setTimeout(function () {
_song += `${i} beers on the wall, you take one down and pass it around, ${
i - 1
} bottles of beer on the wall\n`;
if (i === 1) {
_song += "Hey let's get some more beer";
}
}, 0);
/* eslint-enable no-loop-func */
}
return _song;
}
function singSong(_song) {
if (!_song) throw new Error("song is '' empty, FEED ME A SONG!");
console.log(_song);
}
const song = getSong('beer');
// this will not work
singSong(song);
// Uncaught Error: song is '' empty, FEED ME A SONG!
なぜこれが起こったのですか? `setTimeout`は、CPUに命令をバス上の他の場所に格納するように指示し、データが後でピックアップされるようにスケジュールするように指示します。関数が0ミリ秒マークで再びヒットするまで、数千のCPUサイクルが経過し、CPUはバスから命令をフェッチして実行します。唯一の問題は、song( '')が数千サイクル前に返されたことです。
ファイルシステムとネットワークリクエストの処理でも同じ状況が発生します。メインスレッドを無期限にブロックすることはできません。したがって、コールバックを使用して、制御された方法でコードの実行を時間内にスケジュールします。
次の3つのパターンですべての操作をほぼ実行できます
- **直列:**関数は厳密な順序で実行されます。これは、`for`ループに最も似ています。
// operations defined elsewhere and ready to execute
const operations = [
{ func: function1, args: args1 },
{ func: function2, args: args2 },
{ func: function3, args: args3 },
];
function executeFunctionWithArgs(operation, callback) {
// executes function
const { args, func } = operation;
func(args, callback);
}
function serialProcedure(operation) {
if (!operation) process.exit(0); // finished
executeFunctionWithArgs(operation, function (result) {
// continue AFTER callback
serialProcedure(operations.shift());
});
}
serialProcedure(operations.shift());
- **完全並列:** 1,000,000人のメール受信者のリストにメールを送信する場合など、順序付けが問題にならない場合。
let count = 0;
let success = 0;
const failed = [];
const recipients = [
{ name: 'Bart', email: 'bart@tld' },
{ name: 'Marge', email: 'marge@tld' },
{ name: 'Homer', email: 'homer@tld' },
{ name: 'Lisa', email: 'lisa@tld' },
{ name: 'Maggie', email: 'maggie@tld' },
];
function dispatch(recipient, callback) {
// `sendEmail` is a hypothetical SMTP client
sendMail(
{
subject: 'Dinner tonight',
message: 'We have lots of cabbage on the plate. You coming?',
smtp: recipient.email,
},
callback
);
}
function final(result) {
console.log(`Result: ${result.count} attempts \
& ${result.success} succeeded emails`);
if (result.failed.length)
console.log(`Failed to send to: \
\n${result.failed.join('\n')}\n`);
}
recipients.forEach(function (recipient) {
dispatch(recipient, function (err) {
if (!err) {
success += 1;
} else {
failed.push(recipient.name);
}
count += 1;
if (count === recipients.length) {
final({
count,
success,
failed,
});
}
});
});
- **制限付き並列:**制限付き並列。たとえば、10E7ユーザーのリストから1,000,000人の受信者に正常にメールを送信する場合など。
let successCount = 0;
function final() {
console.log(`dispatched ${successCount} emails`);
console.log('finished');
}
function dispatch(recipient, callback) {
// `sendEmail` is a hypothetical SMTP client
sendMail(
{
subject: 'Dinner tonight',
message: 'We have lots of cabbage on the plate. You coming?',
smtp: recipient.email,
},
callback
);
}
function sendOneMillionEmailsOnly() {
getListOfTenMillionGreatEmails(function (err, bigList) {
if (err) throw err;
function serial(recipient) {
if (!recipient || successCount >= 1000000) return final();
dispatch(recipient, function (_err) {
if (!_err) successCount += 1;
serial(bigList.pop());
});
}
serial(bigList.pop());
});
}
sendOneMillionEmailsOnly();
それぞれに独自のユースケース、利点、および問題があります。詳細については、実験して読んでください。最も重要なことは、操作をモジュール化し、コールバックを使用することです!少しでも疑問がある場合は、すべてミドルウェアであるかのように扱ってください!