问题
您的代码中的问题是这部分:
for (let i = 0; i < collectionNames.length; i++) {
dbase.collection(collectionNames[i].name.toString()).find({}).toArray((err, collectionData) => {
console.log("current collection data: " + collectionData);
allCols[i] = collectionData;
})
}
console.log("done getting all data");
resolve(allCols);
您应该注意到resolve(allCols); 在for 循环结束后立即被调用,但循环的每次迭代不会等待toArray 回调被调用。 p>
dbase.collection(collectionNames[i].name.toString()).find({}).toArray(callback) 行是异步的,因此循环将结束,您将调用 resolve(allCols);,但 .find({}).toArray 代码尚未完成。
解决方案概念
所以,基本上你所做的是:
- 初始化结果数组
allCols = []
- 启动一系列异步操作
- 返回(仍然为空的)结果数组
- 随着异步操作完成,填充现在无用的结果数组。
你应该做的是:
- 启动一系列异步操作
- 等待所有完成
- 从每一个中获取结果
- 返回结果列表
其中的关键是Promise.all([/* array of promises */]) 函数,它接受一个promise 数组并返回一个Promise,它本身向下游传递一个包含所有结果的数组,所以我们需要获取的是这样的:
const dataPromises = []
for (let i = 0; i < collectionNames.length; i++) {
dataPromises[i] = /* data fetch promise */;
}
return Promise.all(dataPromises);
如您所见,最后一行是 return Promise.all(dataPromises); 而不是您代码中的 resolve(allCols),因此我们不能再在 new Promise(func) 构造函数中执行此代码。
相反,我们应该像这样链接Promises 和.then():
getColAsync() {
return this.connectDB().then((db) => {
let dbase = db.db(this.databaseName);
const dataPromises = []
dbase.listCollections().toArray((err, collectionNames) => {
if (err) {
console.log(err);
return Promise.reject(err);
} else {
for (let i = 0; i < collectionNames.length; i++) {
dataPromises[i] = new Promise((res, rej) => {
dbase.collection(collectionNames[i].name.toString()).find({}).toArray((err, collectionData) => {
console.log("current collection data: " + collectionData);
if (err) {
console.log(err);
reject(err);
} else {
resolve(collectionData);
}
});
});
}
console.log("done getting all data");
return Promise.all(dataPromises);
}
});
})
}
注意现在我们返回一个return this.connectDB().then(...),它又返回一个Promise.all(dataPromises);,这在每一步都返回一个新的Promises,让我们保持Promise链的活动,因此getColAsync()本身将返回一个@987654344 @ 然后你可以处理 .then() 和 .catch()。
清洁码
你可以稍微清理一下你的代码:
getColAsync() {
return this.connectDB().then((db) => {
let dbase = db.db(this.databaseName);
const dataPromises = []
// dbase.listCollections().toArray() returns a promise itself
return dbase.listCollections().toArray()
.then((collectionsInfo) => {
// collectionsInfo.map converts an array of collection info into an array of selected
// collections
return collectionsInfo.map((info) => {
return dbase.collection(info.name);
});
})
}).then((collections) => {
// collections.map converts an array of selected collections into an array of Promises
// to get each collection data.
return Promise.all(collections.map((collection) => {
return collection.find({}).toArray();
}))
})
}
如您所见,主要变化是:
- 以承诺形式使用 mondodb 函数
- 使用
Array.map 轻松将数据数组转换为新数组
下面我还展示了你的代码变体,它使用带有回调的函数和我正在开发的模块。
承诺混合
我最近致力于this npm module,以帮助获得更清晰、更易读的 Promises 组合。
在您的情况下,我将使用 fCombine 函数来处理您选择数据库并获取集合信息列表的第一步:
Promise.fCombine({
dbase: (dbURL, done) => mongoClient.connect(dbURL, done),
collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done),
}, { dbURL: this.URL })
这会产生一个承诺,向下游传递一个对象{dbase: /* the db instance */, collInfos: [/* the list of collections info */]}。其中getCollectionNames(dbase, done) 是一个具有如下回调模式的函数:
getCollectionsInfo = (db, done) => {
let dbase = db.db(this.databaseName);
dbase.listCollections().toArray(done);
}
现在您可以链接之前的 Promise 并将集合信息列表转换为选定的数据库集合,如下所示:
Promise.fCombine({
dbase: ({ dbURL }, done) => mongoClient.connect(dbURL, done),
collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done),
}, { dbURL: this.URL }).then(({ dbase, collInfos }) => {
return Promise.resolve(collInfos.map((info) => {
return dbase.collection(info.name);
}));
})
现在下游我们有一个从数据库中选择的集合列表,我们应该从每个集合中获取数据,然后将结果与集合数据合并到一个数组中。
在我的模块中,我有一个 _mux 选项,它创建了一个 PromiseMux,它模仿了常规 Promise 的行为和组合模式,但它实际上同时处理了多个 Promise。每个 Promise 从下游集合数组中获取一个输入项,因此您可以编写代码以从通用集合中获取数据,并将针对数组中的每个集合执行:
Promise.fCombine({
dbase: ({ dbURL }, done) => mongoClient.connect(dbURL, done),
collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done),
}, { dbURL: this.URL }).then(({ dbase, collInfos }) => {
return Promise.resolve(collInfos.map((info) => {
return dbase.collection(info.name);
}));
})._mux((mux) => {
return mux._fReduce([
(collection, done) => collection.find({}).toArray(done)
]).deMux((allCollectionsData) => {
return Promise.resolve(allCollectionsData);
})
});
在上面的代码中,_fReduce 的行为类似于 _fCombine,但它接受一个带有回调的函数数组而不是一个对象,并且它只向下游传递最后一个函数的结果(而不是具有所有结果的结构化对象)。最后deMux 对多路复用器的每个同时的Promise 执行Promise.all,将它们的结果放在一起。
因此整个代码如下所示:
getCollectionsInfo = (db, done) => {
let dbase = db.db(this.databaseName);
dbase.listCollections().toArray(done);
}
getCollAsync = () => {
return Promise.fCombine({
/**
* fCombine uses an object whose fields are functions with callback pattern to
* build consecutive Promises. Each subsequent functions gets as input the results
* from previous functions.
* The second parameter of the fCombine is the initial value, which in our case is
* the db url.
*/
dbase: ({ dbURL }, done) => mongoClient.connect(dbURL, done), // connect to DB, return the connected dbase
collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done), // fetch collection info from dbase, return the info objects
}, { dbURL: this.URL }).then(({ dbase, collInfos }) => {
return Promise.resolve(collInfos.map((info) => {
/**
* we use Array.map to convert collection info into
* a list of selected db collections
*/
return dbase.collection(info.name);
}));
})._mux((mux) => {
/**
* _mux splits the list of collections returned before into a series of "simultaneous promises"
* which you can manipulate as if they were a single Promise.
*/
return mux._fReduce([ // this fReduce here gets as input a single collection from the retrieved list
(collection, done) => collection.find({}).toArray(done)
]).deMux((allCollectionsData) => {
// finally we can put back together all the results.
return Promise.resolve(allCollectionsData);
})
});
}
在我的模块中,我试图避免最常见的反模式思想,但我仍然会处理一些 Ghost Promise。
使用 mongodb 的 Promise 会变得更加简洁:
getCollAsync = () => {
return Promise.combine({
dbase: ({ dbURL }) => { return mongoClient.connect(dbURL); },
collInfos: ({ dbase }) => {
return dbase.db(this.databaseName)
.listCollections().toArray();
},
}, { dbURL: this.URL }).then(({ dbase, collInfos }) => {
return Promise.resolve(collInfos.map((info) => {
return dbase.collection(info.name);
}));
}).then((collections) => {
return Promise.all(collections.map((collection) => {
return collection.find({}).toArray();
}))
});
}