概述
第一次接触编程时,我们就知道了一块代码是从头执行到尾的。 这就是所谓的同步编程:每个操作完成之后,后面的才会继续。 对于不花计算机太多时间的操作,比如数字相加、操作字符串、或变量赋值等等,这种执行过程没什么问题。
但如果一个任务花的时间稍微长一点,你该怎么办呢?比如访问磁盘上的一个文件,发送一个网络请求,或等待一个计时器结束。 在同步编程中,这时候你的程序啥也做不了,只能干等着。
对于一些简单的情况,你的程序可以有多个实例同时在跑,或许还能忍受,但是对于很多服务器应用来说,这就是个噩梦。
进入异步编程 在异步执行的程序中,你的代码在等待某件事的同时可以继续执行,然后这件事发生了你又可以跳回去。
以网络请求为例。 向一个较慢的服务器发送一个网络请求,可能足足要花三秒钟才能响应,你的程序可以在这个慢服务器响应的同时继续干其它的事。 在这个例子中,三秒钟对人来说或许算不了什么,但服务器不一样,它可能还等着响应上千个其它请求呢。 那么,你要如何在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变量的值,如果它有非nul值,则说明有错误发生了。 一般来说,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个文件呢?1个呢? 当前这种写法就太不灵活了。
但别急,我们可以用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