我正在尝试将Promise.all()与即将在foreach循环内填充的Promises数组一起使用,但是Promise.all()似乎没有等待诺言在完成之前全部完成执行其回调。 以下代码有什么问题? (我试图在发布之前简化它,所以它的某些部分可能并不完整,但是承诺和循环就全部存在了。)
class test {
constructor(sql) {
Promise.all([this.sync(sql, 0), this.sync(sql, 1)]).then((data) => {
console.log(data);
});
}
sync(sql, id = 0) {
return new Promise((resolve, reject) => {
request.get('http://localhost/test/' + id, {
json: true
}, (req, res) => {
var promises = [];
res.body['items'].forEach(item => {
promises.push(new Promise((resolve, reject) => {
this.existingRecord(sql, item['id']).then(() => {
resolve(false);
}).catch(() => {
this.add(sql, item).then(resolve(id));
})
}))
});
Promise.all(promises).then((data) => resolve(data));
});
});
}
add(sql, data) {
return new Promise((resolve, reject) => {
console.log('Inserting ' + data['id']);
var request = new sql.Request();
var query = `INSERT INTO test (col1, col2) VALUES (${utils.prepareInsertdata(data)})`;
request.query(query, (err, result) => {
if (err) {
console.log('ERROR INSERTING: ' + data['id']);
console.log(err);
}
resolve();
});
});
}
}
答案 0 :(得分:1)
首先,当您在控制流中混合了promise和常规回调时,编写好,干净,无错误的代码变得更加困难。我发现使用Promise编写异步代码的最佳方法是,首先进行不基于Promise的所有异步操作,然后为它们创建基于Promise的包装,然后仅使用Promise编写逻辑和控制流。这为控制流和错误处理提供了一致的路径,并消除了实际主要逻辑中混杂的东西。
然后,我在您的代码中看到了几个重大问题。
构造函数中的异步操作
在构造函数中放置异步操作几乎从来不是一个好主意。这是因为构造函数必须返回对象本身,以便在异步操作实际完成时以及失败后成功的情况下,没有简单的方法可以与创建对象的代码进行通信。对我来说,尚不清楚您要使用这些异步操作完成什么工作,但这可能是一种不良的设计模式。我更喜欢一个工厂函数,该函数返回一个承诺,该承诺可以解析为新对象,以将对象的创建与异步操作结合在一起。这为您提供了所需的一切,一个完整的对象,何时完成异步操作的知识以及对异步操作进行错误处理的能力。您可以在此处查看有关此工厂功能选项和其他一些设计选项的更多信息:
Asynchronous operations in constructor
改进.then()
处理程序的构造
执行此操作时:
this.add(sql, item).then(resolve(id));
您正在立即调用resolve(id)
并将其传递给.then()
,而不是在调用.then()
之前等待调用resolve(id)
处理程序。所有这些都很复杂,因为您混合了常规的回调和Promise。
创建新的包装承诺,而不仅仅是返回现有承诺
这与常规回调和常规promise的使用有关,但是您宁愿只返回一个现有promise,而不是将其包装在必须手动解决和拒绝的新promise中。超过一半的时间,当您将内容手动包装到新的Promise中时,您会错过适当的错误处理,这只会导致所需的代码更多。
种族条件
在任何类型的多用户数据库环境中,您都无法编写数据库代码,例如:
if (record exists) {
do one thing
} else {
create new record
}
这是比赛条件。如果在处理过程中出现其他数据库请求,则可能会在此过程中更改数据库,您将尝试创建一条由另一段代码创建的记录。
通常的解决方案因数据库而异(并且您不能确切说明正在使用哪个数据库库)。通常,您想让数据库管理唯一记录的创建,从而使数据库中不允许重复记录(通过您在此表中通过其管理唯一性的任何键),并且并发由数据库本身。某些数据库具有原子操作,例如findOrCreate()
,它将以原子方式查找现有记录或创建新记录。其他数据库具有其他方法。但是,重要的是要确保向数据库添加唯一记录是一项原子操作,永远不会创建不需要的重复项。
我建议这种实现方式:
// use promise version of request library (already promisified for us)
const rp = require('request-promise');
class test {
constructor() {
}
init(sql) {
return Promise.all([this.sync(sql, 0), this.sync(sql, 1)]).then((data) => {
console.log(data);
// do something with the data here - probably store it in instance data
});
}
sync(sql, id = 0) {
return rp.get('http://localhost/test/' + id, {json: true}).then(res => {
// process all items
return Promise.all(res.body.items.map(item => {
return this.existingRecord(sql, item.id).then(() => {
return false;
}).catch(() => {
// it's probably bad form here to do this for all possible database errors
// probably this should be looking for a specific error of id not found
// or something like that.
// This is also likely a race condition. You would typically avoid the race
// condition by making the item key unique in the database and just doing an add and letting
// the database tell you the add failed because the item already exists
// This will allow the database to control the concurrency and avoid race conditions
return this.add(sql, item);
});
}));
});
}
}
// factory function that returns promise that resolves to a new object
// don't use new test() elsewhere
function createTestObj(sql) {
let t = new test();
return t.init(sql).then(() => {
// resolve to our new object
return t;
});
}
对于您的add()
方法,我将改用sql数据库中的promise接口。应该有一个内置软件包或一个第三方软件包,它将在数据库界面的顶部添加一个软件包。这样可以防止在add()
方法中手动创建Promise和不完整的错误处理。