10月阅读周·JavaScript异步编程设计快速响应的网络应用:Async.js之异步工作流和数据篇

举报
叶一一 发表于 2024/10/22 09:44:11 2024/10/22
【摘要】 背景去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。没有计划的阅读,收效甚微。新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出1~2个非连续周,完整阅读一本书籍。这个“玩法”虽然常见且板正,但是有效,已经坚持阅读九个月。已读完书籍:《架构简洁之道》、《深入浅出的Node.js》、《你不知道的JavaScript(上卷)》、《你不知道的JavaScri...

背景

去年下半年,我在微信书架里加入了许多技术书籍,各种类别的都有,断断续续的读了一部分。

没有计划的阅读,收效甚微。

新年伊始,我准备尝试一下其他方式,比如阅读周。每月抽出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畅想》等系列作者。华夏美食、国漫、古风重度爱好者,刑侦、无限流小说初级玩家。
如果看完文章有所收获,欢迎点赞👍 | 收藏️ | 留言📝

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。