Return 当第一个承诺解决时

Return when first promise resolves

目标

我在一个数组中有一堆文件名,我想读取第一个存在的文件的内容。它们是配置文件,所以确定顺序很重要,所以我不能使用 .race()。我下面的版本按顺序映射每个文件,尝试加载它,如果加载成功,调用 resolve。

问题

以下是此实现的几个问题:

  1. 调用 resolve(...) 实际上并没有退出循环,所以程序会打开列表中的每个文件,即使不需要。
  2. 拒绝条件(在 this is required to reject when we don't receive any files)似乎是一个 hack。然而,如果它不在这里,承诺永远不会被拒绝。
  3. 解析代码看起来像一个 promise 反模式。

有没有更好的方法来构建这个?我可能可以通过单个 Promise.filter 调用来完成,但如果不需要,我不想查询每个文件。

谢谢

代码

var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));
var _ = require('lodash'); 

new Promise((resolve, reject) => {
    // Resolve with the first of the files below that exists
    return Promise.mapSeries(
        ['./file_that_doesntexist.json', '../file_that_might_exist.json', './file_that_doesnt_exist_either.json', '../file_that_exists.json']
        , (filename) => fs.readFileAsync(filename, 'utf-8')
        .then(file => {
            resolve([filename, file]);
            return true;
        })
        .catch(_.stubFalse)
    )
    .then(files => { // this is required to reject when we don't receive any files
        if(!files.some(x => x))
            reject('did not receive any files');
    });
})
.then(function([filename, configFile]) {
    // do something with filename and configFile
})
.catch(function(err) { 
    console.error(err)
})

如果要顺序迭代,只需使用递归方法:

var Promise = require('bluebird');
var fs = Promise.promisifyAll(require('fs'));

function readFirstOf(filenames)
    if (!filenames.length)
        return Promise.reject(new Error('did not receive any files'));

    return fs.readFileAsync(filenames[0], 'utf-8')
    .then(file =>
        [filenames[0], file]
    , err =>
        readFirstOf(filenames.slice(1))
    );
}

readFirstOf(['./file_that_doesntexist.json', '../file_that_might_exist.json', './file_that_doesnt_exist_either.json', '../file_that_exists.json'])
.then(function([filename, configFile]) {
    // do something with filename and configFile
})
.catch(function(err) { 
    console.error(err)
})

如果你想尝试并行读取它们并且 select 在列表中第一个成功,你可以使用 然后只过滤结果(例如通过 _.find).

这可以通过递归实现,也可以通过使用 Array#reduce() 构建 catch 链来实现:

var paths = ['./file_that_doesntexist.json', '../file_that_might_exist.json', './file_that_doesnt_exist_either.json', '../file_that_exists.json'];

// Resolve with the first of the files below that exists
paths.reduce(function(promise, path) {
    return promise.catch(function(error) {
        return fs.readFileAsync(path, 'utf-8').then(file => [path, file]); 
    });
}, Promise.reject())
.then(function([filename, configFile]) {
    // do something with filename and configFile
})
.catch(function(err) { 
    console.error('did not receive any files', err);
});

捕获链确保每次fs.readFileAsync(path, 'utf-8')失败时,都会尝试下一条路径。

第一个成功的 fs.readFileAsync(path, 'utf-8') 将进入 .then(function([filename, configFile]) {...}

总失败将下降到 .catch(function(err) {...}

有这个 hackish 方法巧妙地解决了这个问题。你可以invert这样的承诺;

var invert = pr => pr.then(v => Promise.reject(v), x => Promise.resolve(x));

当与 Promise.all() 一起使用时,它实际上会派上用场,通过忽略被拒绝的承诺来获得第一个解决承诺。我的意思是,当倒置时,所有被拒绝(已解决)的承诺可能会被忽视,而第一个已解决(拒绝)的承诺会在 Promise.all().catch() 阶段被捕获。酷..!

观看这个;

var invert   = pr => pr.then(v => Promise.reject(v), x => Promise.resolve(x)),
    promises = [Promise.reject("No such file"),
                Promise.reject("No such file either"),
                Promise.resolve("This is the first existing files content"),
                Promise.reject("Yet another missing file"),
                Promise.resolve("Another file content here..!")];
Promise.all(promises.map(pr => invert(pr)))
       .catch(v => console.log(`First successfully resolving promise is: ${v}`));