為何選擇 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%)