async库的部分函数使用心得

发表于 2016-10-24
更新于 2024-05-23
分类于 技术专栏
阅读量 4558
字数统计 6583

前言

不论是在豆米博客网站的controller设计中或者是最近项目的实践中,貌似已经离不开了async库的使用,但貌似每次的使用都不是很顺利,老会出现下面这些常见的错误:

TypeError: callFn.apply is not a function
function () {
        if (fn === null) throw new Error("Callback was already called.");
        var callFn = fn;
        fn = null;
        callFn.apply(this, arguments);

遇到这两个问题的时候不用惊讶,百分百是因为你使用的函数在回调的时候参数不匹配或者函数书写格式不规范造成的。因此今天我们要讲的就是那些常用的async函数的使用方法。在这两个项目中我最常用的函数分为两类:(其他的函数以后有用到的再说,毕竟有实践才理解得深)

  1. 控制流:parallelwaterfall
  2. 集合的操作:eacheachOfmap

1、控制流

控制流的意义便是把握你想要执行的操作的顺序,能够粒度化地控制每一步操作的进度。async提供了很多的控制流函数,目前最常用的便是刚才提到的parallel和waterfall。

1.1、parallel

顾名思义,并行操作,也就是指定你想要执行的操作可以并行地操作。根据官网的解释:

并行地执行集合函数中的任务,不需要等待上一个函数的执行完成。如果任何一个任务传递一个错误给它的回调,那么主`回调`便会直接被调用并带有该错误值。一旦所有的任务完成,那么结果将是以数组的形式传递给最后一个回调。
**注意**:parallel一般是用在那些开始并行I/O任务的,而不是并行地执行代码。如果你的任务中没有使用任何定时器或者执行任何I/O,它们将会串行地执行。每个任务的任何同步设置都会一个接着一个地执行的。毕竟JS是单线程的。
另外我们也可以使用对象而不是数组作为参数。每一个属性可以作为一个函数来运行并且传给最后的回调的结果也是一个对象而不是数组。这对于处理async.parallel的结果貌似更加有可读性。

其调用形式有:

async.paralle([
    function(callback){console.log('first);callback(null, 'return first');},
    function(callback){console.log('second');callback(null, 'return second');},
], function(err, results){
    console.log(results[0]); // return first
    console.log(results[1]); // return second
})

或:

async.paralle({
    first: function(callback){console.log('first);callback(null, 'return first');},
    second: function(callback){console.log('second');callback(null, 'return second');},
}, function(err, results){
    console.log(results); // {first: 'return first', second: 'return second'}
})

Tips

类似于parallel的有parallelLimit函数,该函数对一次并行执行的任务个数有做一个限制。

1.2、waterfall

同样该单词意思是瀑布,那么我们大致就知道该函数是希望它的任务流犹如瀑布那样,是顺序而来。根据官方的解释:

串行地运行数组函数中的任务,每一个任务都可以将其结果传递给下一个任务。但是如果任意一个任务传递错误给它们自己的回调,那么接下去的任务都不会被执行,主回调立即调用该错误。

其调用形式也可以书写成两种方式:

async.waterfall([
    function(callback){console.log('first);callback(null, 'return first');},
    function(result, callback){console.log(result);callback(null, 'return second');},
], function(err, results){
    console.log(results); // return second
})

async.waterfall([
    firstFunction,
    secondFunction,
], function(err, results){
    console.log(results); // return second
})

function firstFunction(callback){
  console.log('first);callback(null, 'return first');
}

function secondFunction(result, callback){
  console.log(result);callback(null, 'return second');
}

2、集合操作

集合操作就是主要操作数组之类的数据集合,有点类似于lodash,不过这个毕竟都是异步的,所以操作起来也是有差别的。

2.1、each

并行地对coll中的每一个item都执行iteratee函数。iteratee函数会被列表的每一个item都调用一次并在完成的时候返回一个回调。如果iteratee传递一个错误给它自己的回调,那么主回调就会立即被调用并且带着这个错误。注意因为这些iteratee是并行调用的,所以无法保证顺序地完成这些iteratee函数。

这个函数是不返回任何结果的,所以它不适用于那些在下一个步骤需要知道结果的情景!

这里提供一种使用mongoose来保存多个refs的写法:

假设这里有School Model和Student Model,二者属于一对多的关系:

    async.each(newStudents, function(student, callback){
      schoolModel.students.push(student);
      callback();
    }, function(err){
      if (err) {
        callback(err);
      }else{
        schoolModel.save(callback);
      }
    });

2.2、eachOf/forEachOf

上面二者的作用是一样的,这个函数和上面的函数功能是一样的,唯一不同的就是多提供了一个索引给iteratee函数,有的时候你可能需要索引,这个时候就需要用这个函数而不是each

Tips:

each衍生的函数还是蛮多的:eachOfLimiteachOfSerieseachSeries。加个limit关键词便是限制每次异步调用时执行的任务的个数。加个Series的话便是表示串行执行异步操作。

2.3、map

刚才提到的each/eachOf都无法将背刺计算的结果以数组的形式返回,那么map函数则具备了这种功能。它与上面两个函数的区别是map(coll, iteratee, callback)的主回调函数带有两个参数:errresultsresults是一个数组,其顺序是按照coll中的item的顺序。比如:(仅供参考不可运行)

async.map(['file1', 'file2', 'file3'], function(file,callback){
  let newFile = {
    name: fileName,
    project_id: projectModel._id
  }
  FileModel.create(newFile, callback);
}, function(err, results){
    
})

3、解决问题

回到刚才前言提到的两个问题,我们在应用这些函数的时候除了需要熟悉他们的调用形式,还需要了解内部执行的函数的结构(尤其是回调的参数个数),比如下面这种情况:

async.waterfall([
  function(newFiles, callback){
    async.each(studentModelArray, function(student, callback){
      schoolModel.students.push(student);
      schoolModel.save(callback);
    }, callback);
  },
  function(school, callback){
    console.log('school is :', school);
    callback();
  }
],function(err){

})

上面的例子的背景大致如下: 我们需要创建许多student model,然后将这些Student model和school model连接起来,使用的是mongoose来操作mongodb。

这样写的后果是什么呢??

程序卡住了,无法继续往下执行

为什么呢?因为async.each的回调执行函数:

 function (err) {
    if (err) console.error(err.message);
    // configs is now a map of JSON data
    doSomethingWith(configs);
}

这样的函数,发现点什么没有?它只有一个参数err,如果没有错误的话,那么接下去调用的函数的参数必须只有callback,也就是说上面的例子中school实际上才是回调,而callback是undefined的,导致程序无法继续执行,改成:

async.waterfall([
  function(newFiles, callback){
    async.each(studentModelArray, function(student, callback){
      schoolModel.students.push(student);
      schoolModel.save(callback);
    }, callback);
  },
  function(callback){
    callback();
  }
],function(err){

})

即可解决,这种因为参数之间的不匹配还有另外一种情况:少参数了! 比如:

async.waterfall([
  function(newFiles, callback){
    async.each(studentModelArray, function(student, callback){
      schoolModel.students.push(student);
      callback();
    }, function(err){
      schoolModel.save(callback);
    });
  },
  function(school, callback){
    console.log(school);
    callback();
  }
],function(err){

})

此时便会报错:

TypeError: callFn.apply is not a function

为什么呢?通过查询mongoose的API文档,你可以发现.save的方法,该save的回调是带有三个参数的:

1. err if an error occurred
2. product which is the saved product
3. numAffected will be 1 when the document was successfully persisted to MongoDB, otherwise 0. Unless you tweak mongoose's internals, you don't need to worry about checking this parameter for errors - checking err is sufficient to make sure your document was properly saved.

所以你需要在下一个调用的函数中传入的参数再加一个,改成:

async.waterfall([
  function(newFiles, callback){
    async.each(studentModelArray, function(student, callback){
      schoolModel.students.push(student);
      callback();
    }, function(err){
      schoolModel.save(callback);
    });
  },
  function(school, numAffected, callback){
    console.log(school);
    callback();
  }
],function(err){

})

如果参数不足,那么async肯定是按照顺序来解析,那么就会发现此时的callback是undefined了,报错那便在所难免的了。

4、结论

综上所述,使用async的库函数只需要清楚两点即可避免无谓的调试时间:

  1. async库函数的使用格式
  2. 内部使用异步操作函数的回调格式

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 async库的部分函数使用心得 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github