如何正确地嵌套使用Promises进行链式操作

6
我的Node项目目前包含一个嵌套回调的侧面圣诞树,以便获取数据并按正确顺序处理它们。现在我正在尝试使用Promises进行重构,但我不确定如何正确地做到这一点。
假设我正在获取办公室列表,然后对于每个办公室获取其所有员工,然后获取每个员工的薪水。最终,所有实体(办公室、员工和薪水)应链接在一起并存储在数据库中。
以下是一些伪代码,说明了我的当前代码(省略了错误处理):
fetch(officesEndpoint, function (data, response) {
    parse(data, function (err, offices) {
        offices.forEach(function (office) {
            save(office);
            fetch(employeesEndPoint, function (data, response) {
                parse(data, function (err, employees) {
                    // link each employee to office
                    save(office);
                    save(employee);
                    employees.forEach(function () {
                        fetch(salaryEndpoint, function (data, response) {
                            parse(data, function (err, salaries) {
                                // link salary to employee
                                save(employee);
                            });
                        });
                    });
                });
            });
        });
    });
});

我尝试使用Promise解决这个问题,但是我遇到了一些问题:

  • 有点啰嗦?
  • 每个办公室都需要与其对应的员工相连,但在saveEmployees函数中,我只能访问从链条上方传递下来的员工,而没有办公室的信息:

var restClient = require('node-rest-client');
var client = new restClient.Client();
var xml2js = require('xml2js');

// some imaginary endpoints
var officesEndpoint = 'http://api/offices';
var employeesEndpoint = 'http://api/offices/employees';
var salaryEndpoint = 'http://api/employees/:id/salary';


function fetch (url) {
    return new Promise(function (resolve, reject) {
        client.get(url, function (data, response) {
            if (response.statusCode !== 200) {
                reject(statusCode);
            }
            resolve(data);
        });
    });
}

function parse (data) {
    return new Promise(function (resolve, reject) {
        xml2js.parseString(data, function (err, result) {
            if (err) {
                reject(err);
            }
            resolve(result);
        });
    });
}

function saveOffices (offices) {
    var saveOffice = function (office) {
        return new Promise(function (resolve, reject) {
            setTimeout(function () {  // simulating async save()
                console.log('saved office in mongodb');
                resolve(office);
            }, 500);
        })
    }
    return Promise.all(offices.map(saveOffice));
}

function saveEmployees (employees) {
    var saveEmployee = function (employee) {
        return new Promise(function (resolve, reject) {
            setTimeout(function () { // simulating async save()
                console.log('saved employee in mongodb');
                resolve(office);
            }, 500);
        })
    }
    return Promise.all(offices.map(saveEmployee));
}

fetch(officesEndpoint)
.then(parse)
.then(saveOffices)
.then(function (savedOffices) {
    console.log('all offices saved!', savedOffices);
    return savedOffices;
})
.then(function (savedOffices) {
    fetch(employeesEndPoint)
    .then(parse)
    .then(saveEmployees)
    .then(function (savedEmployees) {
        // repeat the chain for fetching salaries?
    })
})
.catch(function (error) {
    console.log('something went wrong:', error);
});

1
你正在使用哪个 Promise 库? - Bergi
这个问题不应该属于这个网站吗?http://codereview.stackexchange.com/ - Lewis
@Bergi那是我目前拥有的。承诺不应该使得这更具可维护性和易于推理吗?此外,在嵌套回调中进行错误处理非常不友好。 - user5510778
1
我推荐你阅读关于Promise的这篇文章,它将帮助你找到你正在寻找的解决方案:https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html。 - A. Romeu
1
如果你有机会再次重构代码,我建议你使用ES6生成器来处理异步操作。可以查看这篇文章,它比较了几种JavaScript回调地狱的解决方案。 - hankchiutw
显示剩余6条评论
3个回答

1

您不一定需要嵌套,这种方法也可以:

fetch(officesEndpoint)
  .then(parse)
  .then(saveOffices)
  .then(function(savedOffices) {
    console.log('all offices saved!', savedOffices);
    return savedOffices;
  })
  .then(function(savedOffices) {
    // return a promise
    return fetch(employeesEndPoint); // the returned promise can be more complex, like a Promise.all of fetchEmployeesOfThisOffice(officeId)
  })
  // so you can chain at this level
  .then(parse)
  .then(saveEmployees)
  .then(function(savedEmployees) {
    return fetch(salariesEndPoint);
  })
  .catch(function(error) {
    console.log('something went wrong:', error);
  });

0

将其更改是一个不错的方法

        if (response.statusCode !== 200) {
            reject(statusCode);
        }
        resolve(data);

转换为这个

        if (response.statusCode !== 200) {
            return reject(statusCode);
        }
        resolve(data);

在你的例子中,结果可能是相同的,但如果你正在做更多的事情(比如在数据库中执行某些操作),未预期的结果可能会发生,因为没有返回整个方法将被执行。
这个例子
var prom = new Promise((resolve,reject) => {
    reject(new Error('error'));
    console.log('What? It did not end');
    resolve('Ok, promise will not be called twice');
});

prom.then(val => {
    console.log(val);
}).catch(err => {
    console.log(err.message);
});

正在输出这个结果

What? It did not end
error

对于这个问题 - 如果你需要访问多个返回值(例如办公室和员工),你基本上有两个选择:

  • 嵌套的 promises - 如果“有意义”,这通常不是坏事。虽然 promises 很好地避免了巨大的回调嵌套,但如果逻辑需要,嵌套 promises 是可以的。

  • 使用“全局”变量 - 你可以在 promise 的作用域中定义变量并将结果保存到其中,因此 promises 将使用这些变量作为“全局”(在其作用域内)。


不要使用全局变量。拜托了。有很多更好的方法可以使用 Promise 来实现。这里有一些例子。 - Bergi

0

你的Promise函数 fetch, parse, saveOfficessaveEmployees 都很好。有了它们,你可以重构你当前的代码来使用Promise,适用链式调用代替嵌套,省略一堆错误处理样板代码:

fetch(officesEndpoint)
.then(parse)
.then(function(offices) {
    return Promise.all(offices.map(function(office) {
        return save(office)
        .then(function(){ return fetch(employeesEndPoint); })
        .then(parse)
        .then(function(employees) {
            // link each employee to office
            // throw in a Promise.all([save(office), save(employee)]) if needed here
            return Promise.all(employees.map(function(employee) {
                return fetch(salaryEndpoint)
                .then(parse)
                .then(function(salaries) {
                    return Promise.all(salaries.map(function(salary) {
                        // link salary to employee
                        return save(employee);
                    }));
                });
            }));
        });
    }));
});

在最内层的回调函数中,您可以使用所有的officeemployeesalary来相互链接它们。您无法真正避免这种嵌套。
您将获得一个承诺,用于保存结果的大量数组,或者用于整个过程中的任何错误。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接