10月阅读周·JavaScript异步编程设计快速响应的网络应用:Async.js之异步工作流和数据篇
背景
去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。
没有计划的阅读,收效甚微。
新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。
这个“玩法”虽然常见且板正,但是有效,已经坚持阅读九个月。
已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScript(中卷)》、《你不知道的JavaScript(下卷)》、《数据结构与算法JavaScript描述》、《WebKit技术内幕》、《前端架构:从入门到微前端》、《秒懂算法:用常识解读数据结构与算法》、《JavaScript权威指南》。
当前阅读周书籍:《JavaScript异步编程设计快速响应的网络应用》。
异步工作流的次序问题
假设想先按字母顺序读取recipes(菜谱)目录中的所有文件,接着把读取出的这些内容连接成一个字符串并显示出来。使用同步方法很容易做到这一点。
Asyncjs/synchronous.js
var fs = require('fs');
process.chdir('recipes'); // 改变工作目录
var concatenation = '';
fs.readdirSync('.')
.filter(function (filename) {
// 跳过不是文件的目录
return fs.statSync(filename).isFile();
})
.forEach(function (filename) {
// 内容添加到输出上
concatenation += fs.readFileSync(filename, 'utf8');
});
console.log(concatenation);
(注意,老式的JavaScript环境不支持使用forEach迭代器,如IE6。使用一个像 Kris Kowal之 es5-shim这样的库可以解决这个问题。在第6章中,我们会学到如何只为有需求的浏览器提供库。)
不过,所有这种I/O阻塞的效率都极其低下,尤其是当应用程序还能同时做点其他事情的时候。问题在于不能单纯地将下面这行代码:
concatenation += fs.readFileSync(filename, 'utf8');
换成异步代码:
fs.readFile(filename, 'utf8', function (err, contents) {
if (err) throw err;
concatenation += contents;
});
因为这么做根本无法保证按照做出 readFile 调用的次序来触发readFile 调用的回调。readFile 仅仅负责告诉操作系统开始读取某个文件。对操作系统而言,读取短文件通常比读取长文件更快一些。因此,菜谱内容添加到concatenation字符串的次序是不可预知的。而且,在触发所有回调之后,必须要运行console.log。
要想使用很多异步任务并且希望结果可预知,需要先做一点规划。
异步的数据收集方法
我们先尝试在不借助任何工具函数的情况下来解决这个问题。笔者能想到的最简单的方法是:因前一个 readFile 的回调运行下一个readFile,同时跟踪记录迄今已触发的回调次数,并最终显示输出。下面是笔者的实现结果。
Asyncjs/seriesByHand.js
var fs = require('fs');
process.chdir('recipes'); // 改变工作目录
var concatenation = '';
fs.readdir('.', function (err, filenames) {
if (err) throw err;
function readFileAt(i) {
var filename = filenames[i];
fs.stat(filename, function (err, stats) {
if (err) throw err;
if (!stats.isFile()) return readFileAt(i + 1);
fs.readFile(filename, 'utf8', function (err, text) {
if (err) throw err;
concatenation += text;
if (i + 1 === filenames.length) {
// 所有文件均已读取,可显示输出
return console.log(concatenation);
}
readFileAt(i + 1);
});
});
}
readFileAt(0);
});
如你所见,异步版本的代码要比同步版本多很多。如果使用filter、forEach这些同步方法,代码的行数大约只有一半,而且读起来也要容易得多。如果这些漂亮的迭代器存在异步版本该多好啊!使用Async.js就能做到这一点!
总结
从回调里抛出异常是一种糟糕的设计,尤其在成品环境中。不过,一个简单如斯的示例直接抛出异常则完全没有问题。如果真的遇到代码出错的意外情形,throw会关停代码并提供一个漂亮的堆栈轨迹来解释出错原因。这里真正的不妥之处在于,同样的错误处理逻辑(即if(err) throw err)重复了多达3次!
作者介绍
非职业「传道授业解惑」的开发者叶一一。
《趣学前端》、《CSS畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏⭐️ | 留言📝。
- 点赞
- 收藏
- 关注作者
评论(0)