异步流程控制
本文中的材料主要受到 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 ( === .) {
({
,
,
,
});
}
});
});
每种模式都有其自己的用例、优点和问题,你可以更详细地进行实验和阅读。最重要的是,记住要模块化你的操作并使用回调!如果你有任何疑问,就把所有东西都当作中间件来处理!