这里说并发异步,并不许确,应该说连续异步。NodeJs单线程异步的特性,直接致使多个异步同时进行时,没法肯定最后的执行结果来回调。举个简单的例子:html
for(var i = 0; i < 5; i++) { fs.readFile('file', 'utf-8', function(error, data){}); }
连续发起了5次读文件的异步操做,很简单,那么问题来了,我怎么肯定全部异步都执行完了呢?由于要在它们都执行完后,才能进行以后的操做。相信有点经验的同窗都会想到使用记数的方式来进行,但如何保证记数正确又是一个问题。仔细想一想:数组
回调是一个函数,每一个异步操做时将计数器+1,当每一个异步结束时将计数器-1,经过判断计数器是否为0来肯定是否执行回调。这个逻辑很简单,须要一个相对于执行时和回调时的全局变量做为计数器,并且要在传给异步方法是执行+1的操做,并且以后将返回一个用来回调的函数,有点绕,不过看看Js函数的高级用法:并发
var pending = (function() { var count = 0; return function() { count++; return function() { count--; if (count === 0) { // 所有执行完毕 } } } });
当pending调用时,即pending(),好比:框架
var done = pending();
这时计数变量count即被初始化为0,返回的函数附给了done,这时若是执行done(),会是什么?是否是直接执行pending返回的第一个函数,即:pending()(),这个执行又是什么,首先将计数变量count+1,又返回了一个函数,这个函数直接当作callback传给异步的方法,当执行这个callback的时候,首先是将计数变量count-1,再判断count是否为0,若是为0即表示全部的异步执行完成了,从而达到连续的异步,同一回调的操做。异步
关键就在两个return上,简单的说:async
第一个return的函数是将count+1,接着返回须要回调的函数函数
第二个return的函数就是须要回调的函数,若是它执行,就是将count-1,而后判断异步是否所有执行完成,完成了,就回调spa
看个实际点的例子,读取多个文件的异步回调:线程
var fileName = ['1.html', '2.html', '3.html']; var done = pending(function(fileData) { console.log('done'); console.log(fielData); }); for(var i = 0; i < fileName.lenght; i++) { fs.readFile(fileName[i], 'utf-8', done(fileName[i])); }
其中的done,即用pending方法包起了咱们想回调执行的方法,当计数器为0时,就会执行它,那咱们得改进一下pending方法:code
var pending = (function(callback) { var count = 0; var returns = {}; console.log(count); return function(key) { count++; console.log(count); return function(error, data) { count--; console.log(count); returns[key] = data; if (count === 0) { callback(returns); } } } });
callback即为咱们的回调函数,当var done = pending(callback)时,done其实已为第一个return的函数,它有一个参数,能够当作返回的值的下标,因此在循环体中done(fileName[i]),把文件名传了进去。这个done()是直接执行的,它将count+1后,返回了要传给异步方法的回调函数,如前面所说,这个回调函数里会根据计数变量来判断是否执行咱们但愿执行的回调函数,并且把文件的内容传给了它,即returns。好了,运行一下,相信可以准确的看到运行结果。
0
1
2
3
2
1
0
done
{"1.html": "xxx", "2.html": "xxx", "3.html": "xxx"}
从计数上明显能看出,从0-3再到0,以后就是咱们的回调函数输出了done和文件的内容。
这个问题解决了,咱们要思考一下,如何让这样的方法封装重用,否则,每次都写pending不是很不科学吗?
下面看看UnJs(个人一个基于NodeJs的Web开发框架)的处理方式,应用于模板解析中的子模板操做:
unjs.asyncSeries = function(task, func, callback) { var taskLen = task.length; if (taskLen <= 0) { return; } var done = unjs.pending(callback); for(var i = 0; i < taskLen; i++) { func(task[i], done); } }
asyncSeries有三个参数,意思是:
task: 须要处理的对象,好比须要读取的文件,它是一个列表,若是不是列表,或列表长度为0,它将不会执行
func: 异步方法,好比fs.readFile,就是经过它传进去的
callback: 咱们但愿回调的方法
done和前面同理,它传给了func,但并无执行,由于但愿应用端能可控制参数,因此让应用端去执行。
再看看处理子模板时的操做:
var subTemplate = []; var patt = /\{\% include \'(.+)\' \%\}/ig; while(sub = patt.exec(data)) { var subs = sub; subTemplate.push([subs[0], subs[1]]); } unjs.asyncSeries(subTemplate, function(item, callback) { fs.readFile('./template/' + item[1], 'utf-8', callback(item[0])); }, function(data) { for(var key in data) { html = html.replace(key, data[key]); } });
subTemplate这个列表,是根据对子模板的解析生成的数据,它是一个二维的数组,每一个子项的第一个值为子模板的调用文本,即:{% include 'header.html' %}这样的字符串,第二个参数为子模板文件名,即:header.html
asyncSeries的第二个参数是的callback,其实是第三个参数,也就是咱们但愿执行的回调函数通过pending处理的回调方法,如前面所说,在asyncSeries内部,它并无运行,而是到这里运行的,即:callback(item[0]),带上了参数,由于后面还要根据这个参数将父模板中调用子模板的字符串替换为对应子模板的内容。
这样子,只要须要连续异步时,就可使用asyncSeries方法来处理了。由于异步的关系,程序的流程有点绕,可能开始不太好理解,即便熟悉了,也有可能忽然想不明白,不要紧,好比,第二个参数中的callback实际是第三个参数生成的,开始可能你就会想,这个callback倒底是啥。还有就是pending的两个return,也是不太好理解的,须要多想一想。
好了,连续异步的回调使用Js函数的高级特性完成了。但NodeJs的异步性着实让程序的控制很成问题,诸如还有连续异步,但要传值的操做等,这些都是能够经过这样的思路,变化一下便可实现的。
但愿本文对被NodeJs的异步操做搞得头晕的同窗们有些许的帮助。