使用异步解决回调问题

当我们第一次开始编程时,我们了解到代码块是从上到下执行的。 这是同步编程:每个操作都在下一个操作开始之前完成。 当您要做很多事情而计算机几乎不需要时间来完成时,例如添加数字,操作字符串或分配变量,这非常好。

当您想要花费较长时间执行某项操作(例如访问磁盘上的文件,发送网络请求或等待计时器过去)时,会发生什么情况? 在同步编程中,您的脚本在等待时无法执行其他操作。

对于简单的事情或在脚本的多个实例正在运行的情况下,这可能会很好,但是对于许多服务器应用程序而言,这是一场噩梦。

输入异步编程。 在异步脚本中,您的代码在等待某些事件发生时继续执行,但是在发生某些事件时可以跳回。

以网络请求为例。 如果您向慢速服务器发出网络请求,而该网络请求花了整整三秒钟的时间来响应,那么当此慢速服务器响应时,脚本可能会主动执行其他操作。 在这种情况下,对于人类来说,三秒钟看起来似乎并不多,但是服务器可以在等待时响应数千个其他请求。 那么,如何处理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的文件,则会看到errnull ,并且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
          }
        }
      );
    }
  }
);

该代码看起来很讨厌,并且存在许多问题:

  1. 您正在按顺序加载文件; 如果您可以同时加载它们并在完全加载后返回值,则效率会更高。

  2. 从语法上讲,这是正确的,但很难阅读。 注意嵌套函数的数量和增加的选项卡。 您可以采取一些技巧使它看起来更好一些,但是您可能会以其他方式牺牲可读性。

  3. 这不是很通用。 这对于两个文件来说效果很好,但是如果您有时有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中并不会获得任何收益。

最好从视觉上解释一下:

解释异步编程

这是我们先前写为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.eachasync.map非常相似,但是使用filter可以将布尔值发送到项目回调,而不是文件值。 在完成的回调中,您将获得一个新数组,其中只有在项目回调中传递了truetrue值的元素。

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 errcallback(err)模式非常有用,但是您仍然希望让async.js处理错误。

此外,您会注意到我们将err变量用作回调函数的第一个参数。 乍一看,这看起来不太正确。 但是,由于我们已经检查了err的真实性,因此我们知道传递给回调是虚假且安全的。

在悬崖的边缘

到目前为止,我们已经探索了许多有用的构建块,这些块在同步编程中具有粗略的推论。 让我们直接研究async.waterfall ,它在同步世界中没有很多等效项。

带有瀑布的概念是,一个异步函数的结果流入串联的另一个异步函数的参数中。 这是一个非常强大的概念,尤其是在尝试将多个相互依赖的异步函数串在一起时。 使用async.waterfall ,第一个参数是一个函数数组,第二个参数是您完成的回调。

在您的函数数组中,第一个函数将始终以单个参数(即回调)开头。 每个后续函数应与前一个函数的non-err参数相匹配,但不使用err函数,并添加新的回调。

瀑布的例子

在下一个示例中,我们将开始使用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.asyncifyasync.asyncify包装。 首先,我们将从arrayFsStat函数中获取文件名和stat数组,并使用map对其进行合并。 然后,我们将过滤掉统计数据大小小于300的所有项目。最后,我们将合并的文件名和统计信息对象并再次使用map来取出文件名。

得到大小小于300的文件名后,我们将使用async.mapfs.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

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值