10月阅读周·JavaScript异步编程设计快速响应的网络应用:Async.js篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读九个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》。
当前阅读周书籍:《JavaScript异步编程设计快速响应的网络应用》。
Async.js
Async.js的函数式写法
我们想把同步迭代器所使用的filter和forEach方法替换成相应的异步方法。Async.js给了我们两个选择。
- async.filter 和async.forEach,它们会并行处理给定的数组。
- async.filterSeries 和async.forEachSeries,它们会顺序处理给定的数组。
并行运行这些异步操作应该会更快,那为什么还要使用序列式方法呢?原因有两个。
- 前面提到的工作流次序不可预知的问题。我们确实可以先把结果存储成数组,然后再joining(联接)数组来解决这个问题,但这毕竟多了一个步骤。
- Node 及其他任何应用进程能够同时读取的文件数量有一个上限。如果超过这个上限,操作系统就会报错。如果能顺序读取文件,则无需担心这一限制。
所以现在先搞明白 async.forEachSeries 再说。下面使用了Async.js的数据收集方法,直接改写了同步版本的代码实现。
Asyncjs/forEachSeries.js
var async = require('async');
var fs = require('fs');
process.chdir('recipes'); // 改变工作目录
var concatenation = '';
var dirContents = fs.readdirSync('.');
async.filter(dirContents, isFilename, function (filenames) {
async.forEachSeries(filenames, readAndConcat, onComplete);
});
function isFilename(filename, callback) {
fs.stat(filename, function (err, stats) {
if (err) throw err;
callback(stats.isFile());
});
}
function readAndConcat(filename, callback) {
fs.readFile(filename, 'utf8', function (err, fileContents) {
if (err) return callback(err);
concatenation += fileContents;
callback();
});
}
function onComplete(err) {
if (err) throw err;
console.log(concatenation);
}
现在我们的代码漂亮地分成了两个部分:任务概貌(表现形式为async.filter调用和async.forEachSeries调用)和实现细节(表现形式为两个迭代器函数和一个完工回调onComplete)。
filter 和 forEach 并不是仅有的与标准函数式迭代方法相对应的Async.js工具函数。Async.js还提供了以下方法:
- reject/rejectSeries,与filter 刚好相反;
- map/mapSeries,1:1变换;
- reduce/reduceRight,值的逐步变换;
- detect/detectSeries,找到筛选器匹配的值;
- sortBy,产生一个有序副本;
- some,测试是否至少有一个值符合给定标准;
- every,测试是否所有值均符合给定标准。
这些方法是Async.js的精髓,令你能够以最低的代码重复度来执行常见的迭代工作。在继续探索更高级的方法之前,我们先来看看这些方法的错误处理技术。
Async.js的错误处理技术
简单来说,Async.js遵守Node的约定。这意味着所有的I/O回调都形如callback(err, results……),唯一的例外是结果为布尔型的回调。布尔型回调的写法就是callback(result),所以上一代码示例中的isFilename迭代器需要自己亲自处理错误。
Asyncjs/forEachSeries.js
function isFilename(filename, callback) {
fs.stat(filename, function (err, stats) {
if (err) throw err;
callback(stats.isFile());
});
}
要怪就怪Node的fs.exists首开这一先河吧!而这也意味着使用了Async.js数据收集方法(filter/filterSeries、reject/rejectSeries、detect/detectSeries、some、every 等)的迭代器均无法报告错误。
对于非布尔型的所有Async.js迭代器,传递非null/undefined的值作为迭代器回调的首参数将会立即因该错误值而调用完工回调。这正是readAndConcat不用throw也能工作的原因。
Asyncjs/forEachSeries.js
function readAndConcat(filename, callback) {
fs.readFile(filename, 'utf8', function (err, fileContents) {
if (err) return callback(err);
concatenation += fileContents;
callback();
});
}
所以,如果callback(err)确实是在readAndConcat中被调用的,则这个err会传递给完工回调(即onComplete)。Async.js只负责保证onComplete只被调用一次,而不管是因首次出错而调用,还是因成功完成所有操作而调用。
总结
Node的错误处理约定对 Async.js数据收集方法而言也许并不理想,但对于Async.js的6所有其他方法而言,遵守这些约定可以让错误干净利落地从各个任务流向完工回调。
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)