在尝试编写任何有意义的 Node 应用之前,了解幕后发生的事情很重要。可能需要理解的最重要的一点是 JavaScript——以及扩展 Node——是单线程的。这意味着 Node 应用一次只能做一件事。然而,JavaScript 可以通过使用事件循环给人一种多线程的错觉。事件循环用于在 Node 的事件驱动编程模型中调度任务。每次事件发生时,它都被放入 Node 的事件队列中。在事件循环的每次迭代中,单个事件会出队并被处理。如果在处理过程中,此事件创建了任何其他事件,它们将被简单地添加到队列的末尾。当事件被完全处理后,控制返回到事件循环,并处理另一个事件。
清单 3-1 中的例子说明了事件循环如何允许多个任务并行执行。在本例中,setInterval()用于创建两个周期性任务,每个任务每秒运行一次。第一个任务是显示字符串foo的函数,而第二个任务显示bar。当应用运行时,setInterval()使每个功能大约每 1000 毫秒运行一次。结果是foo和bar每秒打印一次。记住,要执行一个 Node 程序,只需键入"node",后跟程序的文件名。
清单 3-1 。一个给出多线程执行错觉的示例应用
setInterval(function() {
console.log("foo");
}, 1000);
setInterval(function() {
console.log("bar");
}, 1000);基于清单 3-1 中的代码,JavaScript 似乎在同时做多件事。不幸的是,验证它真正的单线程本质太容易了。在清单 3-2 中,一个无限循环被引入到一个重复函数中。无限循环阻止第一个函数返回。因此,控制永远不会传递回事件循环,从而阻止其他任何事情的执行。如果代码是真正多线程的,那么bar将继续被打印到控制台,即使其他函数陷入了无限循环。
清单 3-2 。通过引入无限循环利用 Node 的单线程特性
setInterval(function() {
console.log("foo");
while (true) {
}
}, 1000);
setInterval(function() {
console.log("bar");
}, 1000);异步编程
Node 编程模型的另一个重要方面是几乎所有事情都是异步完成的。异步是如此普遍,以至于许多同步函数在其名称中包含字符串sync以避免混淆。在 Node 的范式下,有时被称为延续传递风格 (CPS)编程,异步函数需要一个额外的参数,这个函数在异步代码完成执行后被调用。这个额外的参数被称为延续,或者更常见的是回调函数。
清单 3-3 中显示了一个异步函数调用的例子。这段代码从文件系统中读取一个文件,并将内容打印到屏幕上。访问文件系统将在本书的后面重新讨论,但是现在,这个例子应该足够简单,容易理解。第一行中导入的核心模块fs用于处理文件系统。readFile()方法异步工作,使用 UTF-8 编码读入文件foo.txt。一旦文件被读取,匿名回调函数被调用。回调函数有两个参数,error和data,它们分别代表错误条件和文件内容。
清单 3-3 。异步文件读取的一个例子
var fs = require("fs");
fs.readFile("foo.txt", "utf8", function(error, data) {
if (error) {
throw error;
}
console.log(data);
});
console.log("Reading file...");这个简短的例子说明了 Node 开发人员的两个重要约定。首先,如果一个方法将回调函数作为参数,那么它应该是最后一个参数。第二,如果一个方法将错误作为参数,它应该是第一个参数。这些不是语言的规则,而是 Node 开发人员社区中普遍认同的调用约定。
当这个程序被执行时,它展示了异步编程的另一个重要方面。为了测试示例程序,将源代码保存在名为file-reader.js的文件中。接下来,在与 Node 脚本相同的目录中创建第二个文件foo.txt。为简单起见,只需将单词"foo"添加到文件中,并保存它。清单 3-4 显示了运行示例程序的输出。注意,消息Reading file...显示在文件内容之前,尽管消息直到最后一行代码才打印出来。
清单 3-4 。文件读取器示例程序的控制台输出
$ node file-reader.js
Reading file...
foo当readFile()被调用时,它对文件系统进行一个非阻塞 I/O 调用。I/O 是非阻塞的这一事实意味着 Node 不等待文件系统返回数据。相反,Node 继续下一条语句,这恰好是一个console.log()调用。最终,文件系统返回foo.txt的内容。发生这种情况时,调用readFile()回调函数,显示文件内容。这种行为似乎与 Node 程序是单线程的事实相矛盾,但是您必须记住,文件系统不是 Node 的一部分。
回调地狱
Node 中使用的 CPS 语法很容易导致被称为回调地狱的情况。当回调嵌套在几个级别的其他回调中时,就会出现回调地狱。这可能导致代码混乱,难以阅读和维护。回调地狱有时被称为末日金字塔,它的名字来自于代码所呈现的金字塔结构。
举个例子,让我们重温一下清单 3-3 中的文件阅读器程序。如果我们要访问一个不存在的文件,就会抛出一个异常,程序就会崩溃。为了使程序更健壮,首先要检查文件是否存在,并且它确实是一个文件(不是目录或其他结构)。修改后的程序如清单 3-5 所示。注意,程序现在包含对fs.exists()和fs.stat()的调用,以及对readFile()的原始调用。由于所有这些都利用了回调函数,代码缩进的级别增加了。将这一点与类似于if语句的结构中的缩进结合起来,您会看到回调地狱如何成为复杂 Node 应用中的一个问题。
清单 3-5 。一个带有回调地狱的文件阅读器程序开始悄悄进入
var fs = require("fs");
var fileName = "foo.txt";
fs.exists(fileName, function(exists) {
if (exists) {
fs.stat(fileName, function(error, stats) {
if (error) {
throw error;
}
if (stats.isFile()) {
fs.readFile(fileName, "utf8", function(error, data) {
if (error) {
throw error;
}
console.log(data);
});
}
});
}
});在本章的后面,你将了解到async,一个可以帮助防止回调地狱的模块。但是,您也可以通过使用小型命名函数作为回调,而不是嵌套的匿名函数来避免这个问题。例如,清单 3-6 重构了清单 3-5 来使用命名函数。注意,对命名函数cbExists()、cbStat()、cbReadFile()的引用已经取代了匿名回调函数。缺点是代码稍长,可能更难理解。对于这么小的应用来说,这可能有点过了,但是对于大型应用来说,这对于整个软件架构来说是必不可少的。
清单 3-6 。重构了文件读取器示例以防止回调崩溃
var fs = require("fs");
var fileName = "foo.txt";
function cbReadFile(error, data) {
if (error) {
throw error;
}
console.log(data);
}
function cbStat(error, stats) {
if (error) {
throw error;
}
if (stats.isFile()) {
fs.readFile(fileName, "utf8", cbReadFile);
}
}
function cbExists(exists) {
if (exists) {
fs.stat(fileName, cbStat);
}
}
fs.exists(fileName, cbExists);异常处理
异步代码对异常处理也有很大的影响。在同步 JavaScript 代码中,try ... catch ... finally语句用于处理错误。然而,Node 的回调驱动特性允许函数在定义它们的错误处理代码之外执行。例如,清单 3-7 将传统的错误处理添加到来自清单 3-3 的文件阅读器示例中。此外,要读取的文件名已被硬编码为空字符串。因此,当调用readFile()时,它无法读取文件并填充回调函数的error参数。然后回调函数抛出错误。直觉上,人们假设catch子句将处理抛出的错误。然而,当回调函数被执行时,try ... catch语句不再是调用堆栈的一部分,异常被置之不理。
清单 3-7 。异步错误处理的错误尝试
var fs = require("fs");
try {
fs.readFile("", "utf8", function(error, data) {
if (error) {
throw error;
}
console.log(data);
});
} catch (exception) {
console.log("The exception was caught!")
}同步异常仍然可以用try...catch...finally语句处理,但是你会发现它们在 Node 中相对无用。大多数 Node 异常都是异步的,可以用多种方式处理。首先,所有接受错误参数的函数都应该检查它——至少清单 3-7 中的例子做到了这一点。在本例中,异常已经被检测到,但随后立即再次被抛出。当然,在实际的应用中,您会希望处理错误,而不是抛出它。
处理异步异常的第二种方法是为流程的uncaughtException事件设置一个全局事件处理程序。Node 提供了一个名为process的全局对象,它与 Node 流程进行交互。当一个未处理的异常一路冒泡回到事件循环时,一个uncaughtException错误被创建。这个异常可以使用process对象的on()方法来处理。清单 3-8 显示了一个全局异常处理程序的例子。
清单 3-8 。全局异常处理程序的示例
var fs = require("fs");
fs.readFile("", "utf8", function(error, data) {
if (error) {
throw error;
}
console.log(data);
});
process.on("uncaughtException", function(error) {
console.log("The exception was caught!")
});虽然全局异常处理程序对于防止崩溃很有用,但是它们不应该用于从错误中恢复。如果处理不当,异常会使您的应用处于不确定的状态。试图摆脱这种状态会带来额外的错误。如果你的程序包含一个全局异常处理程序,那么只使用它来优雅地终止程序。
域
域是处理 Node 中异步错误的首选机制。域,一个相对较新的特性(在 0.8 版本中引入),允许将多个 I/O 操作分组到一个单元中。当一个定时器、事件发射器(在第 4 章的中介绍)或者在一个域中注册的回调函数产生一个错误时,该域会得到通知,这样错误就可以得到适当的处理。
清单 3-9 中的例子展示了域是如何被用来处理异常的。在示例的第二行,导入了domain模块,并创建了一个新的域。然后使用域的run()方法来执行提供的函数。在run()的上下文中,所有的新定时器、事件发射器和回调方法都隐式地注册到域中。当抛出一个错误时,它触发域的错误处理程序。当然,如果没有定义处理函数,异常就会导致程序崩溃。最后,当不再需要该域时,调用它的dispose()方法。
清单 3-9 。使用域的异常处理
var fs = require("fs");
var domain = require("domain").create();
domain.run(function() {
fs.readFile("", "utf8", function(error, data) {
if (error) {
throw error;
}
console.log(data);
domain.dispose();
});
});
domain.on("error", function(error) {
console.log("The exception was caught!")
});显式绑定
如前所述,在run()的上下文中创建的定时器、事件发射器和回调函数被隐式地注册到相应的域中。但是,如果您创建了多个域,那么您可以显式地绑定到另一个域,甚至是在run()的上下文中。例如,清单 3-10 创建了两个域,d1和d2。在d1的run()方法中,创建了一个抛出错误的异步定时器。因为异常发生在d1的run()回调中,所以异常通常由d1处理。然而,定时器是使用add()方法向d2显式注册的。因此,当抛出异常时,d2的错误处理程序被触发。
清单 3-10 。使用域的绑定回调函数示例
var domain = require("domain");
var d1 = domain.create();
var d2 = domain.create();
d1.run(function() {
d2.add(setTimeout(function() {
throw new Error("test error");
}, 1));
});
d2.on("error", function(error) {
console.log("Caught by d2");
});
d1.on("error", function(error) {
console.log("Caught by d1")
});正如我们刚刚看到的,add()用于显式地将定时器绑定到一个域。这也适用于事件发射器。类似的方法remove()从域中删除一个计时器或事件发射器。清单 3-11 展示了如何使用remove()解除一个定时器的绑定。需要注意的非常重要的一点是,从d2中移除timer变量并不会自动将其绑定到d1。相反,由timer的回调函数抛出的异常没有被捕获,程序崩溃。
清单 3-11 。使用remove()解除定时器与域的绑定
var domain = require("domain");
var d1 = domain.create();
var d2 = domain.create();
d1.run(function() {
var timer = setTimeout(function() {
throw new Error("test error");
}, 1);
d2.add(timer);
d2.remove(timer);
});
d2.on("error", function(error) {
console.log("Caught by d2");
});
d1.on("error", function(error) {
console.log("Caught by d1")
});
注意每个域都有一个数组属性members,它包含所有明确添加到域中的定时器和事件发射器。
域还提供了一个bind()方法,可以用来向域显式注册回调函数。这很有用,因为它允许将一个函数绑定到一个域,而不像run()那样立即执行该函数。bind()方法将回调函数作为唯一的参数。返回的函数是原始回调的注册包装。与run()方法一样,异常通过域的错误处理程序来处理。清单 3-12 回顾了使用域bind()方法处理与readFile()回调函数相关的错误的文件阅读器示例。
清单 3-12 。使用域的绑定回调函数示例
var fs = require("fs");
var domain = require("domain").create();
fs.readFile("", "utf8", domain.bind(function(error, data) {
if (error) {
throw error;
}
console.log(data);
domain.dispose();
}));
domain.on("error", function(error) {
console.log("The exception was caught!")
});还有一种方法intercept(),与bind()几乎相同。除了捕捉任何抛出的异常,intercept()还检测任何作为回调函数的第一个参数传递的Error对象。这消除了检查传递给回调函数的任何错误的需要。例如,清单 3-13 使用intercept()方法重写了清单 3-12 。这两个例子行为相同,但是注意在 3-13 中回调不再有error参数。我们还删除了用于检测error参数的if语句。
清单 3-13 。使用域intercept()方法的错误处理
var fs = require("fs");
var domain = require("domain").create();
fs.readFile("", "utf8", domain.intercept(function(data) {
console.log(data);
domain.dispose();
}));
domain.on("error", function(error) {
console.log("The exception was caught!")
});async模块
async是第三方开源模块,对于管理异步控制流非常有用。在撰写本文时,async是npm注册表中第二个最依赖的模块。虽然最初是为 Node 应用开发的,async也可以在客户端使用,因为该模块受到许多流行浏览器的支持,包括 Chrome、Firefox 和 Internet Explorer。开发人员可以提供一个或多个函数,并使用async模块定义它们将如何执行——是串行执行还是以指定的并行度执行。鉴于该模块的受欢迎程度、灵活性和强大功能,async是本书中第一个全面探讨的第三方模块。
串行执行
异步开发最具挑战性的方面之一是在保持代码可读的同时,强制执行函数的顺序。然而,使用async,强制串行执行只是使用series()方法的问题。作为它的第一个参数,series()接受一个数组或对象,其中包含要按顺序执行的函数。每个函数都将回调作为参数。按照 Node 约定,每个回调函数的第一个参数是一个错误对象,如果没有错误,则为null,。回调函数还接受一个可选的第二个参数来表示返回值。调用回调函数导致series()移动到下一个函数。但是,如果有任何函数向它们的回调函数传递错误,那么其余的函数都不会被执行。
series()方法也接受可选的第二个参数,这是在所有函数完成后调用的回调。这个最后的回调接受两个参数,一个错误和一个包含函数结果的数组或对象。如果任何函数向它们的回调函数传递错误,控制会立即传递给最后一个回调函数。
清单 3-14 包含三个定时器任务,每个任务填充results数组的一个元素。在本例中,任务 1 用了 300 毫秒完成,任务 2 用了 200 毫秒,任务 3 用了 100 毫秒。假设我们希望任务按顺序运行,那么需要重新构造代码,以便从任务 2 调用任务 3,而任务 2 又从任务 1 调用任务 3。此外,我们无法知道所有任务何时完成,结果何时准备好。
清单 3-14 。在没有建立控制流的情况下执行定时器任务的示例
var results = [];
setTimeout(function() {
console.log("Task 1");
results[0] = 1;
}, 300);
setTimeout(function() {
console.log("Task 2");
results[1] = 2;
}, 200);
setTimeout(function() {
console.log("Task 3");
results[2] = 3;
}, 100);清单 3-15 显示了运行前一个例子的结果。请注意,任务没有按照正确的顺序执行,也没有办法验证任务返回的结果。
清单 3-15 。验证任务执行顺序错误的控制台输出
$ node timer-tasks
Task 3
Task 2
Task 1清单 3-16 展示了我们如何使用async的series()方法来解决所有与控制流相关的问题,而不会使代码变得复杂。第一行导入了async模块,正如您在第 2 章中了解到的,可以使用命令npm install async 安装该模块。接下来,调用series(),用一组包含原始定时器任务的函数封装在匿名函数中。在每个任务中,期望的返回值作为回调函数的第二个参数传递。对series()的调用还包括一个最终回调函数,它解决了不知道所有结果何时准备好的问题。
清单 3-16 。使用Async串行执行功能的示例
var async = require("async");
async.series([
function(callback) {
setTimeout(function() {
console.log("Task 1");
callback(null, 1);
}, 300);
},
function(callback) {
setTimeout(function() {
console.log("Task 2");
callback(null, 2);
}, 200);
},
function(callback) {
setTimeout(function() {
console.log("Task 3");
callback(null, 3);
}, 100);
}
], function(error, results) {
console.log(results);
});清单 3-17 显示了清单 3-16 的控制台输出,它验证了三个任务是按照指定的顺序执行的。此外,最后的回调提供了检查结果的机制。在这种情况下,结果被格式化为数组,因为任务函数是在数组中传递的。如果使用对象传递任务,结果也会被格式化为对象。
$ node async-series
Task 1
Task 2
Task 3
[ 1, 2, 3 ]处理错误
如前所述,如果任何函数向它们的回调函数传递一个错误,执行会立即短路到最后一个回调函数。在清单 3-18 中,第一个任务中故意引入了一个错误。此外,为了简洁起见,第三个任务已经被删除,最后一个回调现在检查错误。
清单 3-18 。系列示例已经过修改,包含了一个错误
var async = require("async");
async.series([
function(callback) {
setTimeout(function() {
console.log("Task 1");
callback(new Error("Problem in Task 1"), 1);
}, 200);
},
function(callback) {
setTimeout(function() {
console.log("Task 2");
callback(null, 2);
}, 100);
}
], function(error, results) {
if (error) {
console.log(error.toString());
} else {
console.log(results);
}
});引入错误后的结果输出如列表 3-19 所示。请注意,第一个任务中的错误阻止了第二个任务的执行。
清单 3-19 。出现错误时的控制台输出
$ node async-series-error
Task 1
Error: Problem in Task 1并行执行
async模块也可以使用parallel()方法并行执行多个功能。当然,JavaScript 仍然是单线程的,所以您的代码实际上不会并行执行。除了async在调用下一个函数之前不等待一个函数返回之外,parallel()方法的行为与series()完全一样,给人一种并行的错觉。清单 3-20 显示了一个使用parallel()执行同样三个任务的例子。此示例还传递了使用对象中的任务,因为您已经在前面的示例中看到了数组语法。
清单 3-20 。使用Async并行执行三个任务
var async = require("async");
async.parallel({
one: function(callback) {
setTimeout(function() {
console.log("Task 1");
callback(null, 1);
}, 300);
},
two: function(callback) {
setTimeout(function() {
console.log("Task 2");
callback(null, 2);
}, 200);
},
three: function(callback) {
setTimeout(function() {
console.log("Task 3");
callback(null, 3);
}, 100);
}
}, function(error, results) {
console.log(results);
});清单 3-21 显示了来自清单 3-20 的输出。在这种情况下,任务不按程序顺序执行。另外,请注意,显示任务结果的最后一行输出是一个对象,而不是一个数组。
清单 3-21 。并行执行任务的控制台输出
$ node async-parallel
Task 3
Task 2
Task 1
{ three: 3, two: 2, one: 1 }极限平行度
parallel()方法试图尽快执行传递给它的所有函数。一个类似的方法,parallelLimit(),的行为与parallel()完全一样,除了您可以为并行执行的任务数量设置一个上限。清单 3-22 显示了一个parallelLimit()方法的使用示例。在这种情况下,并行度限制设置为 2,在最终回调之前使用一个额外的参数。需要注意的是,parallelLimit()不会在 n 的离散批次中执行功能。相反,该函数只是确保永远不会有超过 n 个函数同时执行。
清单 3-22 。并行执行三个任务,最大并行度为 2
var async = require("async");
async.parallelLimit({
one: function(callback) {
setTimeout(function() {
console.log("Task 1");
callback(null, 1);
}, 300);
},
two: function(callback) {
setTimeout(function() {
console.log("Task 2");
callback(null, 2);
}, 200);
},
three: function(callback) {
setTimeout(function() {
console.log("Task 3");
callback(null, 3);
}, 100);
}
}, 2, function(error, results) {
console.log(results);
});清单 3-23 显示了来自清单 3-22 的结果输出。请注意,任务 1 和 2 在第三个任务之前完成,尽管它的计时器延迟最小。这表明任务 3 直到前两个任务中的一个完成后才开始执行。
清单 3-23 。运行清单 3-22 中的代码的输出
$ node parallel-limit.js
Task 2
Task 1
Task 3
{ two: 2, one: 1, three: 3 }瀑布模型
瀑布模型 是一种串行模型,当任务依赖于先前完成的任务的结果时,这种模型很有用。瀑布也可以被认为是装配线,每个任务执行一个更大的任务的一部分。瀑布是使用async方法waterfall()创建的。设置瀑布与使用series()或parallel()非常相似。然而,有几个关键的区别。首先,组成瀑布的函数列表只能存储在一个数组中(不支持对象符号)。第二个关键区别是,只有最后一个任务的结果被传递给最终的回调函数。第三个区别是任务函数可以接受前一个任务提供的附加参数。
清单 3-24 显示了一个瀑布的例子。它使用勾股定理来计算三角形斜边的长度。勾股定理指出,对于直角三角形,斜边的平方长度等于其他两条边的平方之和。定理一般写成a2+b2=c2,其中 c 为斜边的长度。在清单 3-24 中,使用waterfall()方法将问题分解为三个任务。第一个任务创建两个随机数作为值 a 和 b 。这些值被传递给任务的回调函数,从而使它们成为第二个任务的前两个参数。第二个任务计算 a 和 b 的平方和,并将该值传递给第三个任务。第三个任务计算传递给它的值的平方根。这个值,斜边的长度,被传递给最终的回调函数,在那里被打印到控制台。
清单 3-24 。计算直角三角形斜边长度的瀑布
var async = require("async");
async.waterfall([
function(callback) {
callback(null, Math.random(), Math.random());
},
function(a, b, callback) {
callback(null, a * a + b * b);
},
function(cc, callback) {
callback(null, Math.sqrt(cc));
}
], function(error, c) {
console.log(c);
});排队模型
async也支持使用queue()方法的任务队列。与以前的执行模型不同,以前的执行模型执行许多作为参数传入的函数,队列模型允许您在执行过程中的任何时候动态添加任务。队列对于解决生产者-消费者类型的问题很有用。因为 JavaScript 是单线程的,所以您可以放心地忽略生产者-消费者问题中通常会出现的潜在并发问题。
清单 3-25 显示了一个async队列的基本初始化。队列对象是使用queue()方法创建的,该方法将任务处理函数作为输入参数。任务处理程序接受两个参数,一个用户定义的任务和一个回调函数,一旦任务被处理,就应该用一个错误参数调用该回调函数。在这个例子中,没有发生错误,所以调用回调函数,用null作为它的参数。与parallelLimit()方法类似,queue()方法也采用一个参数来指定队列的并行级别。清单 3-25 中所示的队列可以同时处理多达四个任务。
清单 3-25 。初始化一个async队列
var async = require("async");
var queue = async.queue(function(task, callback) {
// process the task argument
console.log(task);
callback(null);
}, 4);一旦建立了队列,就开始使用它的push()和unshift()方法向它添加任务。与同名的数组方法一样,unshift()和push()分别将任务添加到队列的开头和结尾。这两种方法都可以将单个任务添加到队列中,或者通过传入数组将多个任务添加到队列中。两种方法都接受可选的回调函数;如果存在,则在每个任务完成后,将使用错误参数调用它。
清单 3-26 。向async队列添加任务的例子
var i = 0;
setInterval(function() {
queue.push({
id: i
}, function(error) {
console.log("Finished a task");
});
i++;
}, 200);其他队列方法和属性
在任何时候,您都可以通过使用length()方法来确定队列中元素的数量。您还可以使用concurrency属性来控制队列的并行级别。例如,如果队列长度超过了一个阈值,您可以使用清单 3-27 中所示的代码来增加并发任务的数量。
清单 3-27 。根据负载更新队列的Concurrency
if (queue.length() > threshold) {
queue.concurrency = 8;
}队列还支持许多回调函数,这些函数在某些事件发生时被触发。这些回调函数是saturated()、empty()和drain()。每当队列的长度等于它的并发性时,就会触发saturated()函数,每当从队列中移除最后一个任务时,就会调用empty(),当最后一个任务处理完毕时,就会调用drain()。清单 3-28 中显示了每个函数的示例。
清单 3-28 。saturated()、empty()和drain()的使用示例
queue.saturated = function() {
console.log("Queue is saturated");
};
queue.empty = function() {
console.log("Queue is empty");
};
queue.drain = function() {
console.log("Queue is drained");
};重复方法
async模块还提供了其他方法,这些方法重复调用一个函数,直到满足某个条件。其中最基本的是whilst(),它的行为类似于一个while循环。清单 3-29 展示了如何使用whilst()来实现一个异步while循环。whilst()方法将三个函数作为参数。第一个是同步真值测试,它没有参数,在每次迭代之前被检查。传递给whilst()的第二个函数在每次真值测试返回true时执行。这个函数将回调作为它唯一的参数,并且可以被认为是循环体。循环体的回调函数将一个可选的错误作为其唯一的参数,在本例中该参数被设置为null。一旦真值测试返回false,就执行whilst()的第三个参数,并作为最终的回调函数。这个函数也将一个可选的错误作为它唯一的参数。
清单 3-29 。使用whilst() 实现简单循环
var async = require("async");
var i = 0;
async.whilst(function() {
return i < 5;
}, function(callback) {
setTimeout(function() {
console.log("i = " + i);
i++;
callback(null);
}, 1000);
}, function(error) {
console.log("Done!");
});重复变化
async模块提供了三种额外的方法来实现异步的类循环结构。这些方法是doWhilst()、until()和doUntil(),它们的行为几乎和whilst()一模一样。第一个doWhilst(),是一个do-while循环的异步等价物,until()是whilst()的逆,一直执行到真值测试返回true。类似地,doUntil()是doWhilst()的逆,只要真值测试返回false就执行。这些方法的签名如清单 3-30 所示。请注意,body参数出现在doWhilst()和doUntil()的test之前。
清单 3-30 。doWhilst() 、until() 、doUntil() 的方法签名
async.doWhilst(body, test, callback)
async.until(test, body, callback)
async.doUntil(body, test, callback)附加async功能
async除了已经介绍的功能之外,还提供了许多其他实用功能。例如,async提供了实现记忆化的memoize()和unmemoize()方法。该模块还提供了用于处理集合的许多常用方法的串行和并行版本。这些方法包括each()、map()、filter()、reduce()、some()和every()。在模块的 GitHub 页面上可以找到async提供的方法以及参考代码的完整列表:https://github.com/caolan/async。
注记忆化是一种编程技术,它试图通过缓存函数先前计算的结果来提高性能。当调用记忆化函数时,它的输入参数被映射到软件缓存中的输出。下次使用相同的输入调用该函数时,将返回缓存的值,而不是再次执行该函数。
摘要
本章已经开始探索 Node 编程模型。阅读本章后,您应该对异步编程和非阻塞 I/O 的概念有了更好的理解。如果您仍然不确定,请返回并再次阅读该章。如果您计划进行任何严肃的 Node 开发,理解这些概念是绝对必要的。异常处理(也在这里讨论)可能会被推迟到以后,但是由于异步错误处理可能是一个棘手的问题,所以最好尽快将其提上日程。
本章还介绍了现有最流行的 Node 模块之一async。在任何 Node 开发者的工具箱中,async都是一个非常强大的工具,它也可以在浏览器中工作,这也使它成为前端开发者的资产。使用async提供的模型,几乎可以抽象出任何执行模式。此外,模型可以嵌套在其他模型中。例如,您可以创建一组并行执行的函数,每个函数包含一个嵌套的瀑布。