为何选择 Streams

让我们检查以下两个用于读取文件内容的示例:

第一个,它使用异步方法读取文件,并提供一个回调函数,一旦文件完全读入内存就调用该函数:

fs.readFile(`${__dirname}/utils.js`, (err, data) => {
  if (err) {
    handleError(err);
  } else {
    console.log(data.toString());
  }
})

第二个,它使用 streams 来逐个读取文件的内容:

var fileStream = fs.createReadStream(`${__dirname}/file`);
var fileContent = '';
fileStream.on('data', data => {
  fileContent += data.toString();
})

fileStream.on('end', () => {
  console.log(fileContent);
})

fileStream.on('error', err => {
  handleError(err)
})

值得一提的是,这两个例子完全相同。那有什么区别呢?

  • 第一个更短,看起来更优雅
  • 第二个让你做一些处理上的文件被阅读(!)

当你处理的文件很小时,使用 streams 时没有实际效果,但是当文件很大时会发生什么? (这么大,需要 10 秒才能将其读入内存)

如果没有 streams,你将等待,绝对不做任何事情(除非你的进程做其他事情),直到 10 秒钟通过并且文件被完全读取,然后才能开始处理文件。

使用 streams,你可以一目了然地获取文件的内容,只要它们可用 - 并且可以让你在读取文件处理该文件。

上面的例子没有说明如何将 streams 用于进行回调方式时无法完成的工作,所以让我们看另一个例子:

我想下载一个 gzip 文件,解压缩并将其内容保存到磁盘。鉴于文件的 url,这是需要做的:

  • 下载文件
  • 解压缩文件
  • 将其保存到磁盘

这是一个[小文件] [1],存储在我的 S3 存储中。以下代码以回调方式执行上述操作。

var startTime = Date.now()
s3.getObject({Bucket: 'some-bucket', Key: 'tweets.gz'}, (err, data) => {
  // here, the whole file was downloaded

  zlib.gunzip(data.Body, (err, data) => {
    // here, the whole file was unzipped

    fs.writeFile(`${__dirname}/tweets.json`, data, err => {
      if (err) console.error(err)

      // here, the whole file was written to disk
      var endTime = Date.now()
      console.log(`${endTime - startTime} milliseconds`) // 1339 milliseconds
    })
  })
})

// 1339 milliseconds

这是使用 streams 看起来的样子:

s3.getObject({Bucket: 'some-bucket', Key: 'tweets.gz'}).createReadStream()
  .pipe(zlib.createGunzip())
  .pipe(fs.createWriteStream(`${__dirname}/tweets.json`));

// 1204 milliseconds

是的,处理小文件时速度并不快 - 测试文件权重 80KB。在一个更大的文件上测试这个,71MB gzipped(382MB 解压缩),表明 streams 版本更快

  • 下载 71MB,解压缩然后将 382MB 写入磁盘花了 20925 毫秒 - 使用回调方式
  • 相比之下,使用 streams 版本时需要 13434 毫秒才能完成相同的操作(对于不那么大的文件,速度提高 35%)