当我们第一次开始编程时,我们了解到代码块是从上到下执行的。 这是同步编程:每个操作都在下一个操作开始之前完成。 当您要做很多事情而计算机几乎不需要时间来完成时,例如添加数字,操作字符串或分配变量,这非常好。
当您想要花费较长时间执行某项操作(例如访问磁盘上的文件,发送网络请求或等待计时器过去)时,会发生什么情况? 在同步编程中,您的脚本在等待时无法执行其他操作。
对于简单的事情或在脚本的多个实例正在运行的情况下,这可能会很好,但是对于许多服务器应用程序而言,这是一场噩梦。
输入异步编程。 在异步脚本中,您的代码在等待某些事件发生时继续执行,但是在发生某些事件时可以跳回。
以网络请求为例。 如果您向慢速服务器发出网络请求,而该网络请求花了整整三秒钟的时间来响应,那么当此慢速服务器响应时,脚本可能会主动执行其他操作。 在这种情况下,对于人类来说,三秒钟看起来似乎并不多,但是服务器可以在等待时响应数千个其他请求。 那么,如何处理Node.js中的异步?
最基本的方法是通过回调。 回调只是异步操作完成时调用的函数。 按照约定,Node.js回调函数至少具有一个参数err
。 回调可能具有更多参数(通常表示返回到回调的数据),但是第一个参数是err
。 正如您可能已经猜到的那样, err
持有一个错误对象(如果已触发错误,请稍后再介绍)。
让我们看一个非常简单的例子。 我们将使用Node.js的内置文件系统模块( fs
)。 在此脚本中,我们将读取文本文件的内容。 文件的最后一行是console.log
,它提出了一个问题:如果运行此脚本,您认为在我们看到文本文件的内容之前会看到日志吗?
var
fs = require('fs');
fs.readFile(
'a-text-file.txt', //the filename of a text file that says "Hello!"
'utf8', //the encoding of the file, in this case, utf-8
function(err,text) { //the callback
console.log('Error:',err); //Errors, if any
console.log('Text:',text); //the contents of the file
}
);
//Will this be before or after the Error / Text?
console.log('Does this get logged before or after the contents of the text file?');
由于这是异步的,因此我们实际上会在文本文件内容之前看到最后一个console.log
。 如果在执行节点脚本的同一目录中有一个名为a-text-file.txt的文件,则会看到err
为null
,并且text
的值填充了该文本文件的内容。
如果没有名为a-text-file.txt的文件,则 err
将返回Error对象,并且text
的值将是undefined
。 这导致了回调的重要方面:您必须始终处理错误。 要处理错误,您需要检查err
的值 变量; 如果存在值,则发生错误。 按照惯例, err
参数通常不返回false
,因此您只能检查真实性。
var
fs = require('fs');
fs.readFile(
'a-text-file.txt', //the filename of a text file that says "Hello!"
'utf8', //the encoding of the file, in this case, utf-8
function(err,text) { //the callback
if (err) {
console.error(err); //display an error to the console
} else {
console.log('Text:',text); //no error, so display the contents of the file
}
}
);
现在,假设您要以特定顺序显示两个文件的内容。 您最终将得到以下内容:
var
fs = require('fs');
fs.readFile(
'a-text-file.txt', //the filename of a text file that says "Hello!"
'utf8', //the encoding of the file, in this case, utf-8
function(err,text) { //the callback
if (err) {
console.error(err); //display an error to the console
} else {
console.log('First text file:',text); //no error, so display the contents of the file
fs.readFile(
'another-text-file.txt', //the filename of a text file that says "Hello!"
'utf8', //the encoding of the file, in this case, utf-8
function(err,text) { //the callback
if (err) {
console.error(err); //display an error to the console
} else {
console.log('Second text file:',text); //no error, so display the contents of the file
}
}
);
}
}
);
该代码看起来很讨厌,并且存在许多问题:
您正在按顺序加载文件; 如果您可以同时加载它们并在完全加载后返回值,则效率会更高。
-
从语法上讲,这是正确的,但很难阅读。 注意嵌套函数的数量和增加的选项卡。 您可以采取一些技巧使它看起来更好一些,但是您可能会以其他方式牺牲可读性。
-
这不是很通用。 这对于两个文件来说效果很好,但是如果您有时有9个文件而其他时候是22个或只有一个,那该怎么办? 当前的编写方式非常严格。
不用担心,我们可以使用async.js解决所有这些问题(以及更多)。
使用Async.js进行回调
首先,让我们开始安装async.js模块。
npm install async —-save
Async.js可以用于将一系列函数以串行或并行方式粘合在一起。 让我们重写我们的示例:
var
async = require('async'), //async.js module
fs = require('fs');
async.series( //execute the functions in the first argument one after another
[ //The first argument is an array of functions
function(cb) { //`cb` is shorthand for "callback"
fs.readFile(
'a-text-file.txt',
'utf8',
cb
);
},
function(cb) {
fs.readFile(
'another-text-file.txt',
'utf8',
cb
);
}
],
function(err,values) { //The "done" callback that is ran after the functions in the array have completed
if (err) { //If any errors occurred when functions in the array executed, they will be sent as the err.
console.error(err);
} else { //If err is falsy then everything is good
console.log('First text file:',values[0]);
console.log('Second text file:',values[1]);
}
}
);
它的工作原理几乎与前面的示例一样,顺序加载每个文件,不同之处仅在于它读取每个文件,直到完成才显示结果。 该代码比之前的示例更简洁,更简洁(稍后我们将使其变得更好)。 async.series
接受一系列函数并一个接一个地执行它们。
每个函数应该只有一个参数,即回调(或我们代码中的cb
)。 cb
应该使用与任何其他回调相同的参数类型执行,因此我们可以将其正确放入fs.readFile
参数中。
最后,结果被发送到最终的回调,即async.series
的第二个参数。 结果存储在数组中,其值与async.series
的第一个参数中的函数顺序相关。
使用async.js可以简化错误处理,因为如果遇到错误,它将把错误返回到最终回调的参数,并且将不再执行任何其他异步函数。
现在都在一起了
一个相关的函数是async.parallel
; 它具有与async.series
相同的参数,因此您可以在两者之间进行更改而无需更改其余语法。 这是涵盖并行与并发的好地方。
JavaScript本质上是一种单线程语言,这意味着它一次只能做一件事。 它能够在单独的线程中执行某些任务(例如,大多数I / O函数),而这正是JS进行异步编程的地方。 不要将并行与并发混淆。
当您使用async.parallel
执行两项操作时,您并没有使它打开另一个线程来解析JavaScript或一次执行两项操作-您实际上是在async.parallel
的第一个参数中控制它何时在函数之间传递。 因此,仅将同步代码放在async.parallel中并不会获得任何收益。
最好从视觉上解释一下:
![解释异步编程](https://cms-assets.tutsplus.com/uploads/users/1197/posts/26591/image/g4694.png)
这是我们先前写为parallel的示例,唯一的区别是我们使用async.parallel
而不是async.series
。
var
async = require('async'), //async.js module
fs = require('fs');
async.parallel( //execute the functions in the first argument, but don't wait for the first function to finish to start the second
[ //The first argument is an array of functions
function(cb) { //`cb` is shorthand for "callback"
fs.readFile(
'a-text-file.txt',
'utf8',
cb
);
},
function(cb) {
fs.readFile(
'another-text-file.txt',
'utf8',
cb
);
}
],
function(err,values) { //The "done" callback that is ran after the functions in the array have completed
if (err) { //If any errors occurred when functions in the array executed, they will be sent as the err.
console.error(err);
} else { //If err is falsy then everything is good
console.log('First text file:',values[0]);
console.log('Second text file:',values[1]);
}
}
);
一遍又一遍
我们前面的示例执行了固定数量的操作,但是如果您需要可变数量的异步操作会怎样? 如果您仅依靠回调和常规语言构造,依靠笨拙的计数器或条件检查(使代码的真实含义难以理解),这将很快变得混乱。 让我们看一下async.js的for循环的大致含义。
在此示例中,我们将使用顺序文件名和一些简短内容将十个文件写入当前目录。 您可以通过更改async.times
的第一个参数的值来改变async.times
。 在此示例中, fs.writeFile
的回调仅创建一个err
参数,但是async.times
函数也可以支持返回值。 像async.series一样,它作为数组传递给第二个参数中的done回调。
var
async = require('async'),
fs = require('fs');
async.times(
10, // number of times to run the function
function(runCount,callback) {
fs.writeFile(
'file-'+runCount+'.txt', //the new file name
'This is file number '+runCount, //the contents of the new file
callback
);
},
function(err) {
if (err) {
console.error(err);
} else {
console.log('Wrote files.');
}
}
);
现在是时候说大多数async.js函数默认并行运行而不是串行运行了。 因此,在上面的示例中,它将开始创建文件并在完全创建和写入所有文件时报告。
默认情况下,并行运行的那些函数具有一个推论性的系列函数,该函数以“系列”结尾(如您所猜)。 因此,如果您想以串行方式而不是并行方式运行此示例,则可以将async.times
更改为async.timesSeries
。
对于下一个循环示例,我们将看一下async.until函数。 async.until
(连续)执行异步功能,直到满足特定条件为止。 该函数采用三个函数作为参数。
第一个函数是测试,在此测试中返回true(如果要停止循环)或false(如果要继续循环)。 第二个参数是异步函数,最后一个是完成的回调。 看一下这个例子:
var
async = require('async'),
fs = require('fs'),
startTime = new Date().getTime(), //the unix timestamp in milliseconds
runCount = 0;
async.until(
function () {
//return true if 4 milliseconds have elapsed, otherwise false (and continue running the script)
return new Date().getTime() > (startTime + 5);
},
function(callback) {
runCount += 1;
fs.writeFile(
'timed-file-'+runCount+'.txt', //the new file name
'This is file number '+runCount, //the contents of the new file
callback
);
},
function(err) {
if (err) {
console.error(err);
} else {
console.log('Wrote files.');
}
}
);
该脚本将创建新的文本文件,持续时间为五毫秒。 在脚本开始时,我们以毫秒为单位获取unix纪元的开始时间,然后在test函数中获得当前时间,并测试是否比开始时间加5毫秒大5毫秒。 如果多次运行此脚本,则可能会得到不同的结果。
在我的机器上,我在5毫秒内创建了6到20个文件。 有趣的是,如果您尝试将console.log
添加到测试功能或异步功能中,则会得到截然不同的结果,因为写入控制台需要花费时间。 它只是向您显示,在软件中,一切都有性能成本!
for each循环是一个方便的结构—它允许您为数组的每个项目做一些事情。 在async.js中,这将是async.each
函数。 该函数带有三个参数:集合或数组,为每个项目执行的异步函数以及完成的回调。
在下面的示例中,我们获取一个字符串数组(在这种情况下,是猎犬的类型),并为每个字符串创建一个文件。 创建所有文件后,将执行完成的回调。 如您所料,错误是通过完成回调中的err
对象处理的。 async.each
是并行运行的,但是如果要串行运行,可以遵循前面提到的模式,并使用async.eachSeries
代替async.each
。
var
async = require('async'),
fs = require('fs');
async.each(
//an array of sighthound dog breeds
['greyhound','saluki','borzoi','galga','podenco','whippet','lurcher','italian-greyhound'],
function(dogBreed, callback) {
fs.writeFile(
dogBreed+'.txt', //the new file name
'file for dogs of the breed '+dogBreed, //the contents of the new file
callback
);
},
function(err) {
if (err) {
console.error(err);
} else {
console.log('Done writing files about dogs.');
}
}
);
async.each
的表亲是async.map
函数; 区别在于您可以将值传递回完成的回调。 使用async.map
函数,您将数组或集合作为第一个参数传递,然后异步函数将在数组或集合中的每个项目上运行。 最后一个参数是完成的回调。
下面的示例采用狗的品种数组,并使用每一项创建文件名。 然后将文件名传递给fs.readFile
,在文件名中读取该文件名,并通过回调函数将这些值传递回。 最后,在完成的回调参数中包含文件内容的数组。
var
async = require('async'),
fs = require('fs');
async.map(
['greyhound','saluki','borzoi','galga','podenco','whippet','lurcher','italian-greyhound'],
function(dogBreed, callback) {
fs.readFile(
dogBreed+'.txt', //the new file name
'utf8',
callback
);
},
function(err, dogBreedFileContents) {
if (err) {
console.error(err);
} else {
console.log('dog breeds');
console.log(dogBreedFileContents);
}
}
);
async.filter
在语法上也与async.each
和async.map
非常相似,但是使用filter可以将布尔值发送到项目回调,而不是文件值。 在完成的回调中,您将获得一个新数组,其中只有在项目回调中传递了true
或true值的元素。
var
async = require('async'),
fs = require('fs');
async.filter(
['greyhound','saluki','borzoi','galga','podenco','whippet','lurcher','italian-greyhound'],
function(dogBreed, callback) {
fs.readFile(
dogBreed+'.txt', //the new file name
'utf8',
function(err,fileContents) {
if (err) { callback(err); } else {
callback(
err, //this will be falsy since we checked it above
fileContents.match(/greyhound/gi) //use RegExp to check for the string 'greyhound' in the contents of the file
);
}
}
);
},
function(err, dogBreedFileContents) {
if (err) {
console.error(err);
} else {
console.log('greyhound breeds:');
console.log(dogBreedFileContents);
}
}
);
在此示例中,我们比前面的示例做更多的事情。 注意我们如何添加一个附加的函数调用并处理我们自己的错误。 if
需要操作异步函数的结果, if
err
和callback(err)
模式非常有用,但是您仍然希望让async.js处理错误。
此外,您会注意到我们将err变量用作回调函数的第一个参数。 乍一看,这看起来不太正确。 但是,由于我们已经检查了err的真实性,因此我们知道传递给回调是虚假且安全的。
在悬崖的边缘
到目前为止,我们已经探索了许多有用的构建块,这些块在同步编程中具有粗略的推论。 让我们直接研究async.waterfall
,它在同步世界中没有很多等效项。
带有瀑布的概念是,一个异步函数的结果流入串联的另一个异步函数的参数中。 这是一个非常强大的概念,尤其是在尝试将多个相互依赖的异步函数串在一起时。 使用async.waterfall
,第一个参数是一个函数数组,第二个参数是您完成的回调。
在您的函数数组中,第一个函数将始终以单个参数(即回调)开头。 每个后续函数应与前一个函数的non-err参数相匹配,但不使用err函数,并添加新的回调。
![瀑布的例子](https://i-blog.csdnimg.cn/blog_migrate/da326e19ddb48a1ec104476ed96b949e.png)
在下一个示例中,我们将开始使用Waterfall作为胶水来组合一些概念。 在这是第一个参数数组,我们有三个功能:首先加载目录从当前目录列表,第二需要的目录列表,并使用async.map
运行fs.stat
上的每个文件,第三个功能需要第一个函数结果中的目录列表,并获取每个文件的内容( fs.readFile
)。
async.waterfall
按顺序运行每个函数,因此它将始终运行所有fs.stat
函数,然后再运行任何fs.readFile
。 在第一个示例中,第二个和第三个函数彼此不依赖,因此可以将它们包装在async.parallel
以减少总执行时间,但是在下一个示例中,我们将再次修改此结构。
注意: 在文本文件的一个小目录中运行此示例,否则您将在终端窗口中长时间获取大量垃圾。
var
async = require('async'),
fs = require('fs');
async.waterfall([
function(callback) {
fs.readdir('.',callback); //read the current directory, pass it along to the next function.
},
function(fileNames,callback) { //`fileNames` is the directory listing from the previous function
async.map(
fileNames, //The directory listing is just an array of filenames,
fs.stat, //so we can use async.map to run fs.stat for each filename
function(err,stats) {
if (err) { callback(err); } else {
callback(err,fileNames,stats); //pass along the error, the directory listing and the stat collection to the next item in the waterfall
}
}
);
},
function(fileNames,stats,callback) { //the directory listing, `fileNames` is joined by the collection of fs.stat objects in `stats`
async.map(
fileNames,
function(aFileName,readCallback) { //This time we're taking the filenames with map and passing them along to fs.readFile to get the contents
fs.readFile(aFileName,'utf8',readCallback);
},
function(err,contents) {
if (err) { callback(err); } else { //Now our callback will have three arguments, the original directory listing (`fileNames`), the fs.stats collection and an array of with the contents of each file
callback(err,fileNames,stats,contents);
}
}
);
}
],
function(err, fileNames,stats,contents) {
if (err) {
console.error(err);
} else {
console.log(fileNames);
console.log(stats);
console.log(contents);
}
}
);
假设我们只想获取大小超过500字节的文件的结果。 我们可以使用上面的代码,但是无论您是否需要,都可以获取每个文件的大小和内容。 您如何仅获取文件的统计信息,而仅获取满足大小要求的文件内容?
首先,我们可以将所有匿名函数提取到命名函数中。 这是个人喜好,但是使代码更简洁,更易于理解(可重用引导)。 如您所料,您将需要获取大小,评估这些大小,并仅获取超出大小要求的文件内容。 可以使用Array.filter
东西轻松完成此Array.filter
,但这是一个同步函数,并且async.waterfall需要异步样式的函数。 Async.js有一个辅助函数,可以将同步函数包装为异步函数,即相当漂亮的async.asyncify
。
我们需要做三件事,所有这些我们async.asyncify
用async.asyncify
包装。 首先,我们将从arrayFsStat
函数中获取文件名和stat数组,并使用map
对其进行合并。 然后,我们将过滤掉统计数据大小小于300的所有项目。最后,我们将合并的文件名和统计信息对象并再次使用map
来取出文件名。
得到大小小于300的文件名后,我们将使用async.map
和fs.readFile
来获取内容。 有很多方法可以破解这个鸡蛋,但在我们的案例中,它被分解以显示最大的灵活性和代码重用性。 这种async.waterfall
用法说明了如何混合和匹配同步和异步代码。
var
async = require('async'),
fs = require('fs');
//Our anonymous refactored into named functions
function directoryListing(callback) {
fs.readdir('.',callback);
}
function arrayFsStat(fileNames,callback) {
async.map(
fileNames,
fs.stat,
function(err,stats) {
if (err) { callback(err); } else {
callback(err,fileNames,stats);
}
}
);
}
function arrayFsReadFile(fileNames,callback) {
async.map(
fileNames,
function(aFileName,readCallback) {
fs.readFile(aFileName,'utf8',readCallback);
},
function(err,contents) {
if (err) { callback(err); } else {
callback(err,contents);
}
}
);
}
//These functions are synchronous
function mergeFilenameAndStat(fileNames,stats) {
return stats.map(function(aStatObj,index) {
aStatObj.fileName = fileNames[index];
return aStatObj;
});
}
function above300(combinedFilenamesAndStats) {
return combinedFilenamesAndStats
.filter(function(aStatObj) {
return aStatObj.size >= 300;
});
}
function justFilenames(combinedFilenamesAndStats) {
return combinedFilenamesAndStats
.map(function(aCombinedFileNameAndStatObj) {
return aCombinedFileNameAndStatObj.fileName;
});
}
async.waterfall([
directoryListing,
arrayFsStat,
async.asyncify(mergeFilenameAndStat), //asyncify wraps synchronous functions in a err-first callback
async.asyncify(above300),
async.asyncify(justFilenames),
arrayFsReadFile
],
function(err,contents) {
if (err) {
console.error(err);
} else {
console.log(contents);
}
}
);
更进一步,让我们进一步完善我们的功能。 假设我们要编写一个功能完全相同的函数,但是可以灵活地查看任何路径。 async.seq是async.seq
。 尽管async.waterfall
仅执行功能的瀑布,但async.seq
返回执行其他功能的瀑布的功能。 除了创建一个函数外,您还可以传入将传入第一个异步函数的值。
转换为async.seq
只需进行一些修改。 首先,我们将修改directoryListing
以接受参数-这将是路径。 其次,我们将添加一个变量来保存我们的新函数( directoryAbove300
)。 第三,我们将从async.waterfall
获取数组参数,并将其转换为async.seq
参数。 现在,当我们运行directoryAbove300
时,对瀑布的完成回调将用作完成回调。
var
async = require('async'),
fs = require('fs'),
directoryAbove300;
function directoryListing(initialPath,callback) { //we can pass a variable into the first function used in async.seq - the resulting function can accept arguments and pass them this first function
fs.readdir(initialPath,callback);
}
function arrayFsStat(fileNames,callback) {
async.map(
fileNames,
fs.stat,
function(err,stats) {
if (err) { callback(err); } else {
callback(err,fileNames,stats);
}
}
);
}
function arrayFsReadFile(fileNames,callback) {
async.map(
fileNames,
function(aFileName,readCallback) {
fs.readFile(aFileName,'utf8',readCallback);
},
function(err,contents) {
if (err) { callback(err); } else {
callback(err,contents);
}
}
);
}
function mergeFilenameAndStat(fileNames,stats) {
return stats.map(function(aStatObj,index) {
aStatObj.fileName = fileNames[index];
return aStatObj;
});
}
function above300(combinedFilenamesAndStats) {
return combinedFilenamesAndStats
.filter(function(aStatObj) {
return aStatObj.size >= 300;
});
}
function justFilenames(combinedFilenamesAndStats) {
return combinedFilenamesAndStats
.map(function(aCombinedFileNameAndStatObj) {
return aCombinedFileNameAndStatObj.fileName;
})
}
//async.seq will produce a new function that you can use over and over
directoryAbove300 = async.seq(
directoryListing,
arrayFsStat,
async.asyncify(mergeFilenameAndStat),
async.asyncify(above300),
async.asyncify(justFilenames),
arrayFsReadFile
);
directoryAbove300(
'.',
function(err, fileNames,stats,contents) {
if (err) {
console.error(err);
} else {
console.log(fileNames);
}
}
);
关于承诺和异步功能的注意事项
您可能想知道为什么我没有提到承诺 。 我没有反对他们的想法-它们很方便,并且可能比回调更优雅的解决方案-但是它们是查看异步编码的另一种方法。
内置的Node.js模块使用err
first回调,而其他数千个模块则使用此模式。 实际上,这就是本教程在示例中使用fs
的原因-与Node.js中的文件系统访问使用回调一样基本,因此在没有承诺的情况下整理回调代码是Node.js编程的重要组成部分。
可以使用诸如Bluebird之类的东西来将err-first回调包装到基于Promise的函数中 ,但这只能使您步入正轨-Async.js提供了许多隐喻,使异步代码易于读取和管理。
拥抱异步
JavaScript已成为在Web上工作的事实语言之一。 它并非没有学习曲线,并且有许多框架和库也让您忙碌。 如果您正在寻找其他资源来学习或在工作中使用,请查看Envato市场中可用的资源 。
但是学习异步是完全不同的,希望本教程向您展示了它的实用性。
异步是编写服务器端JavaScript的关键,但是,如果编写不正确,则您的代码可能会成为无法管理的回调野兽。 通过使用像async.js这样的提供许多隐喻的库,您可能会发现编写异步代码很有趣。
翻译自: https://code.tutsplus.com/tutorials/solving-callback-problems-with-async--cms-26591