【问题标题】:Refactor synchronous code to unleash the power of node.js asynchronicity重构同步代码以释放 node.js 异步性的力量
【发布时间】:2018-01-23 21:54:00
【问题描述】:

我们长期从事 Python 和 PHP 编码的人有一些整洁的同步代码(示例如下)。大多数函数都有异步对应项。我们真的很想“获得” Javascript 和 Node 的强大功能,并相信这是异步 node.js 可以加快速度并让我们大吃一惊的理想案例。

重构以下内容以利用异步节点的教科书方法是什么? Async / awaitpromise.all?如何? (使用 Node 8.4.0。向后兼容性不是问题。)

var fs = require('fs');

// This could list over 10,000 files of various size 
const fileList = ['file1', 'file2', 'file3']; 

const getCdate = file => fs.statSync(file).ctime; // Has async method

const getFSize = file => fs.statSync(file).size; // Has async method

// Can be async through file streams (see resources below)
const getMd5 = (file) => {
  let fileData = new Buffer(0);
  fileData = fs.readFileSync(file);
  const hash = crypto.createHash('md5');
  hash.update(fileData);
  return hash.digest('hex');
};

let filesObj = fileList.map(file => [file, {
  datetime: getCdate(file),
  filesize: getFSize(file),
  md5hash: getMd5(file),
}]);

console.log(filesObj);

注意事项:

  • 我们需要保持函数的模块化和可重用性。
  • filesObj 获取内容的函数比此处列出的要多
  • 大多数函数可以重写为异步,有些则不能。
  • 理想情况下,我们需要保持fileList 的原始顺序。
  • 理想情况下,我们希望使用最新的 Node 和 JS 功能,而不是依赖外部模块。

异步获取md5的各种文件流方法:

【问题讨论】:

  • 没有解释的反对票是没有用的。请帮助改进问题。
  • 我希望一个反对票弹出一个框,要求用户至少输入一个简短的理由。

标签: node.js asynchronous


【解决方案1】:

我会让getCDategetFSizegetMd5 全部异步并承诺化,然后将它们包装在另一个异步承诺返回函数中,这里称为statFile

function statFile(file) {
    return Promise.all([
        getCDate(file),
        getFSize(file),
        getMd5(file)
    ]).then((datetime, filesize, md5hash) => ({datetime, filesize, md5hash}))
    .catch(/*handle error*/);
}

然后您可以将映射功能更改为

const promises = fileList.map(statFile);

那么使用Promise.all就简单了:

Promise.all(promises)
    .then(filesObj => /*do something*/)
    .catch(err => /*handle error*/)

这使事情变得模块化,不需要 async/await,允许您将额外的功能插入 statFile,并保留您的文件顺序。

【讨论】:

  • 要从箭头函数返回一个对象,您需要将它包裹在括号中,这样它就不会将括号误认为是代码块的开头。 (datetime, filesize, md5hash) => ({datetime, filesize, md5hash}); :)
  • @Chev 谢谢,我不知道这个问题的答案。
【解决方案2】:

您可以通过多种不同的方式异步处理此代码。您可以使用节点async 库来更优雅地处理所有回调。如果您不想深入承诺,那么这就是“简单”的选择。我在引号中加上了easy,因为如果你足够了解promise,它们实际上会更容易。异步库很有帮助,但在错误传播方面仍有很多不足之处,并且您必须将所有调用都包含在其中的大量样板代码。

更好的方法是使用 Promise。 Async/Await 仍然很新。如果没有像 Bable 或 Typescript 这样的预处理器,甚至不支持节点 7(不确定节点 8)。此外,async/await 无论如何都会在底层使用 Promise。

这是我使用 Promise 的方法,甚至包括文件统计缓存以实现最佳性能:

const fs = require('fs');
const crypto = require('crypto');
const Promise = require('bluebird');
const fileList = ['file1', 'file2', 'file3'];

// Use Bluebird's Promise.promisifyAll utility to turn all of fs'
// async functions into promise returning versions of them.
// The new promise-enabled methods will have the same name but with
// a suffix of "Async". Ex: fs.stat will be fs.statAsync.
Promise.promisifyAll(fs);

// Create a cache to store the file if we're planning to get multiple
// stats from it.
let cache = {
  fileName: null,
  fileStats: null
};
const getFileStats = (fileName, prop) => {
  if (cache.fileName === fileName) {
    return cache.fileStats[prop];
  }
  // Return a promise that eventually resolves to the data we're after
  // but also stores fileStats in our cache for future calls.
  return fs.statAsync(fileName).then(fileStats => {
    cache.fileName = fileName;
    cache.fileStats = fileStats;
    return fileStats[prop];
  })
};

const getMd5Hash = file => {
  // Return a promise that eventually resolves to the hash we're after.
  return fs.readFileAsync(file).then(fileData => {
    const hash = crypto.createHash('md5');
    hash.update(fileData);
    return hash.digest('hex');
  });
};

// Create a promise that immediately resolves with our fileList array.
// Use Bluebird's Promise.map utility. Works very similar to Array.map 
// except it expects all array entries to be promises that will
// eventually be resolved to the data we want.
let results = Promise.resolve(fileList).map(fileName => {
  return Promise.all([

    // This first gets a promise that starts resolving file stats
    // asynchronously. When the promise resolves it will store file
    // stats in a cache and then return the stats value we're after.
    // Note that the final return is not a promise, but returning raw
    // values from promise handlers implicitly does
    // Promise.resolve(rawValue)
    getFileStats(fileName, 'ctime'),

    // This one will not return a promise. It will see cached file
    // stats for our file and return the stats value from the cache
    // instead. Since it's being returned into a Promise.all, it will
    // be implicitly wrapped in Promise.resolve(rawValue) to fit the
    // promise paradigm.
    getFileStats(fileName, 'size'),

    // First returns a promise that begins resolving the file data for
    // our file. A promise handler in the function will then perform
    // the operations we need to do on the file data in order to get
    // the hash. The raw hash value is returned in the end and
    // implicitly wrapped in Promise.resolve as well.
    getMd5(file)
  ])
  // .spread is a bluebird shortcut that replaces .then. If the value
  // being resolved is an array (which it is because Promise.all will
  // resolve an array containing the results in the same order as we
  // listed the calls in the input array) then .spread will spread the
  // values in that array out and pass them in as individual function
  // parameters.
  .spread((dateTime, fileSize, md5Hash) => [file, { dateTime, fileSize, md5Hash }]);
}).catch(error => {
  // Any errors returned by any of the Async functions in this promise
  // chain will be propagated here.
  console.log(error);
});

这是代码,但没有 cmets 以便于查看:

const fs = require('fs');
const crypto = require('crypto');
const Promise = require('bluebird');
const fileList = ['file1', 'file2', 'file3'];

Promise.promisifyAll(fs);

let cache = {
  fileName: null,
  fileStats: null
};
const getFileStats = (fileName, prop) => {
  if (cache.fileName === fileName) {
    return cache.fileStats[prop];
  }
  return fs.statAsync(fileName).then(fileStats => {
    cache.fileName = fileName;
    cache.fileStats = fileStats;
    return fileStats[prop];
  })
};

const getMd5Hash = file => {
  return fs.readFileAsync(file).then(fileData => {
    const hash = crypto.createHash('md5');
    hash.update(fileData);
    return hash.digest('hex');
  });
};

let results = Promise.resolve(fileList).map(fileName => {
  return Promise.all([
    getFileStats(fileName, 'ctime'),
    getFileStats(fileName, 'size'),
    getMd5(file)
  ]).spread((dateTime, fileSize, md5Hash) => [file, { dateTime, fileSize, md5Hash }]);
}).catch(console.log);

最终的结果将是一个类似的数组,它有望与您的原始代码的结果相匹配,但在基准测试中应该表现得更好:

[
  ['file1', { dateTime: 'data here', fileSize: 'data here', md5Hash: 'data here' }],
  ['file2', { dateTime: 'data here', fileSize: 'data here', md5Hash: 'data here' }],
  ['file3', { dateTime: 'data here', fileSize: 'data here', md5Hash: 'data here' }]
]

如有任何错别字,请提前致歉。没有时间或能力实际运行任何这些。不过我仔细看了一遍。


在发现 async/await 从 7.6 开始就在节点中后,我决定昨晚玩一下它。对于不需要并行完成的递归异步任务,或者您可能希望可以同步编写的嵌套异步任务,它似乎最有用。对于您在这里需要的东西,我可以看到没有任何令人兴奋的使用 async/await 的方法,但是有一些地方的代码可以更清晰地阅读。这是代码,但有一些小的异步/等待便利。

const fs = require('fs');
const crypto = require('crypto');
const Promise = require('bluebird');
const fileList = ['file1', 'file2', 'file3'];

Promise.promisifyAll(fs);

let cache = {
  fileName: null,
  fileStats: null
};
async function getFileStats (fileName, prop) {
  if (cache.fileName === fileName) {
    return cache.fileStats[prop];
  }
  let fileStats = await fs.stat(fileName);
  cache.fileName = fileName;
  cache.fileStats = fileStats;
  return fileStats[prop];
};

async function getMd5Hash (file) {
  let fileData = await fs.readFileAsync(file);
  const hash = crypto.createHash('md5');
  hash.update(fileData);
  return hash.digest('hex');
};

let results = Promise.resolve(fileList).map(fileName => {
  return Promise.all([
    getFileStats(fileName, 'ctime'),
    getFileStats(fileName, 'size'),
    getMd5(file)
  ]).spread((dateTime, fileSize, md5Hash) => [file, { dateTime, fileSize, md5Hash }]);
}).catch(console.log);

【讨论】:

  • 太棒了。感谢您的想法。 async / await 我相信自 7.6 以来就是节点的一部分,并且承诺也是标准的。不要认为我们需要 bluebird(不确定 promisfy),我们更愿意尽可能“原生”地做事(更少的模块)。
  • 基本的 promises 做了很多,但我更喜欢方便的方法。 .spreadpromisifyAllPromise.map。尽管 node 现在支持基本的 Promise,但 Bluebird 仍然很常用。不过,您当然可以在没有 Bluebird 的情况下完成上述大部分操作。 .spread 可以只是 .then,您可以手动从结果数组中读取。承诺fs 函数虽然有点样板。 Bluebird 的 Promise.map 必须用手动 array.map 替换为 promise 数组。
  • 也感谢关于 async/await 的提示。不知道我已经可以检查出来了! :D
  • 昨晚我探索了 async/await 并在您的代码中发现了一些有用的地方。主要在用于获取文件统计信息或文件内容的辅助函数中。这些函数现在看起来更具可读性和同步性。我尝试在 Promise.all 逻辑中添加 async/await 糖,但实际上我觉得它使那部分代码变得臃肿,而不是帮助任何事情,因为它正在并行处理递归异步代码。我在答案中添加了另一个编辑,显示了我的更改:)
猜你喜欢
  • 1970-01-01
  • 2021-08-05
  • 1970-01-01
  • 2013-05-11
  • 1970-01-01
  • 2020-11-05
  • 1970-01-01
  • 1970-01-01
  • 2017-02-04
相关资源
最近更新 更多