非同步流程控制
本文中的材料主要受到 Mixu 的 Node.js Book 的啟發。
JavaScript 的核心設計是在“主”執行緒上非阻塞,檢視正是在這個執行緒上渲染的。你可以想象這在瀏覽器中的重要性。當主執行緒被阻塞時,就會導致終端使用者所恐懼的臭名昭著的“凍結”現象,並且沒有其他事件可以被分派,從而導致例如資料採集的丟失。
這產生了一些獨特的約束,只有函數語言程式設計風格才能解決。這就是回撥函式的用武之地。
然而,在更復雜的操作中,處理回撥可能會變得很有挑戰性。這通常會導致“回撥地獄”,即多個帶有回撥的巢狀函式使得程式碼更難閱讀、除錯和組織等。
async1(function (, ) {
async2(function () {
async3(function () {
async4(function () {
async5(function () {
// do something with output
});
});
});
});
});
當然,在現實生活中,很可能還會有額外的程式碼行來處理 result1、result2 等,因此,這個問題的長度和複雜性通常會導致程式碼看起來比上面的例子要混亂得多。
這就是*函式*發揮巨大作用的地方。更復雜的操作是由許多函式組成的。
- 啟動器風格 / 輸入
- 中介軟體
- 終止器
“啟動器風格/輸入”是序列中的第一個函式。這個函式將接受操作的原始輸入(如果有的話)。操作是一系列可執行的函式,原始輸入將主要是
- 全域性環境中的變數
- 帶或不帶引數的直接呼叫
- 透過檔案系統或網路請求獲得的值
網路請求可以是由外部網路發起的傳入請求,也可以是由同一網路上的另一個應用程式發起的請求,或者是應用程式自身在相同或外部網路上發起的請求。
中介軟體函式將返回另一個函式,而終止器函式將呼叫回撥。下圖說明了網路或檔案系統請求的流程。這裡的延遲為 0,因為所有這些值都在記憶體中可用。
function (, ) {
(`${} and terminated by executing callback `);
}
function (, ) {
return (`${} touched by middleware `, );
}
function () {
const = 'hello this is a function ';
(, function () {
.();
// requires callback to `return` result
});
}
();
狀態管理
函式可能依賴於狀態,也可能不依賴於狀態。當函式的輸入或其他變數依賴於外部函式時,就會產生狀態依賴。
這樣,狀態管理主要有兩種策略:
- 直接將變數傳入函式,以及
- 從快取、會話、檔案、資料庫、網路或其他外部來源獲取變數值。
請注意,我沒有提到全域性變數。使用全域性變數管理狀態通常是一種糟糕的反模式,它使得保證狀態變得困難甚至不可能。在複雜的程式中應儘可能避免使用全域性變數。
控制流
如果一個物件在記憶體中可用,迭代是可能的,並且控制流不會發生改變。
function () {
let = '';
let = 100;
for (; > 0; -= 1) {
+= `${} beers on the wall, you take one down and pass it around, ${
- 1
} bottles of beer on the wall\n`;
if ( === 1) {
+= "Hey let's get some more beer";
}
}
return ;
}
function () {
if (!) {
throw new ("song is '' empty, FEED ME A SONG!");
}
.();
}
const = ();
// this will work
();
然而,如果資料存在於記憶體之外,迭代將不再起作用。
function () {
let = '';
let = 100;
for (; > 0; -= 1) {
(function () {
+= `${} beers on the wall, you take one down and pass it around, ${
- 1
} bottles of beer on the wall\n`;
if ( === 1) {
+= "Hey let's get some more beer";
}
}, 0);
}
return ;
}
function () {
if (!) {
throw new ("song is '' empty, FEED ME A SONG!");
}
.();
}
const = ('beer');
// this will not work
();
// 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 = [
{ : function1, : args1 },
{ : function2, : args2 },
{ : function3, : args3 },
];
function (, ) {
// executes function
const { , } = ;
(, );
}
function () {
if (!) {
.(0); // finished
}
(, function () {
// continue AFTER callback
(.());
});
}
(.());
- 有限序列: 函式將按嚴格的順序執行,但有執行次數的限制。當您需要處理一個大列表,但對成功處理的專案數量有上限時,這很有用。
let = 0;
function () {
.(`dispatched ${} emails`);
.('finished');
}
function (, ) {
// `sendMail` is a hypothetical SMTP client
sendMail(
{
: 'Dinner tonight',
: 'We have lots of cabbage on the plate. You coming?',
: .email,
},
);
}
function () {
getListOfTenMillionGreatEmails(function (, ) {
if () {
throw ;
}
function () {
if (! || >= 1000000) {
return ();
}
(, function () {
if (!) {
+= 1;
}
(.pop());
});
}
(.pop());
});
}
();
- 完全並行: 當順序不重要時,例如向 1,000,000 個電子郵件收件人列表傳送電子郵件。
let = 0;
let = 0;
const = [];
const = [
{ : 'Bart', : 'bart@tld' },
{ : 'Marge', : 'marge@tld' },
{ : 'Homer', : 'homer@tld' },
{ : 'Lisa', : 'lisa@tld' },
{ : 'Maggie', : 'maggie@tld' },
];
function (, ) {
// `sendMail` is a hypothetical SMTP client
sendMail(
{
: 'Dinner tonight',
: 'We have lots of cabbage on the plate. You coming?',
: .email,
},
);
}
function () {
.(`Result: ${.count} attempts \
& ${.success} succeeded emails`);
if (.failed.length) {
.(`Failed to send to: \
\n${.failed.join('\n')}\n`);
}
}
.(function () {
(, function () {
if (!) {
+= 1;
} else {
.(.);
}
+= 1;
if ( === .) {
({
,
,
,
});
}
});
});
每種模式都有其自己的用例、優點和問題,你可以更詳細地進行實驗和閱讀。最重要的是,記住要模組化你的操作並使用回撥!如果你有任何疑問,就把所有東西都當作中介軟體來處理!