这是我的一项简单任务:通过id数组查找图像并将图像值渲染到模板中。
router.get('/gallery', function(req, res) {
var images = [];
imagesIds.forEach(function(eachImageId) {
Images.findById(eachImageId).exec(function(findImageErr, foundImage) {
if (foundImage) {
images.push(foundImage);
}
});
});
res.render('gallery', {
images: images
});
});
问题是'res.render'函数不等待'findById'函数完成。 'images'数组总是变成'[]'空。
我尝试使用发电机但不知道如何实现。
如果有人可以解释没有库(如q)会更好。因为我想深入了解发电机如何处理这个问题。
答案 0 :(得分:2)
生成器允许编写类似于同步的函数,因为它们可以停止执行并在以后恢复它。
我猜你已经读过一些像this这样的文章了解如何定义生成器函数并使用它们。
您的异步代码可以表示为带有魔术yield
关键字的简单迭代器。生成器函数将在此处运行并停止,直到您使用方法next()
恢复它。
function* loadImages(imagesIds) {
var images = [], image;
for(imageId of imagesIds) {
image = yield loadSingleImage(imageId);
images.push(image);
}
return images;
}
因为有一个循环,所以函数将在每个next()
的循环中运行,直到所有imagesIds
都已经走过。最后将执行return语句,您将获得images
。
现在我们需要描述图像加载。我们的生成器函数需要知道当前图像何时已加载并且可以开始下载。所有现代javascript运行时(node.js和最新浏览器)都具有本机Promise
对象支持,我们将定义一个返回promise的函数,如果找到它将最终用图像解析。
function loadSingleImage(imageId) {
return new Promise((resolve, reject) => {
Images.findById(imageId).exec((findImageErr, foundImage) => {
if (foundImage) {
resolve(foundImage)
} else {
reject();
}
});
});
}
我们有两个功能,一个用于单个图像加载,另一个用于将它们组合在一起。现在我们需要一个调度程序来将控制从一个函数传递到另一个函数。由于您不想使用库,我们必须自己实施一些帮助。
它是spawn function的较小版本,可以更简单,更易于理解,因为我们不需要处理错误,只是忽略丢失的图像。
function spawn(generator) {
function continuer(value) {
var result = generator.next(value);
if(!result.done) {
return Promise.resolve(result.value).then(continuer);
} else {
return result.value;
}
}
return continuer();
}
此函数在continuer
函数内执行我们的生成器的递归调用,而result.done
不为真。一旦获得,这意味着生成已成功完成,我们可以返回我们的价值。
最后,将所有内容放在一起,您将获得以下用于库加载的代码。
router.get('/gallery', function(req, res) {
var imageGenerator = loadImages(imagesIds);
spawn(imageGenerator).then(function(images) {
res.render('gallery', {
images: images
});
});
});
现在loadImages
函数中有一些伪同步代码。我希望了解生成器的工作原理有所帮助。
另请注意,所有图像都会被后续加载,因为我们等待
loadSingleImage
调用的异步结果将其放入数组中,然后我们才能转到下一个imageId
。如果您打算在生产中使用这种方式,它可能会导致性能问题。
相关链接:
答案 1 :(得分:1)
如你所知,它可以在没有第三方的情况下完成,但这会很麻烦...... 无论如何,底线是在回调函数"函数(findImageErr,foundImage){...}"。
内完成。1)如果没有第三方,您只需在考虑所有图像后进行渲染:
var images = [];
var results=0;
imagesIds.forEach(function(eachImageId) {
Images.findById(eachImageId).exec(function(findImageErr, foundImage) {
results++;
if(foundImage)
images.push(foundImage);
if(results == imagesIds.length)
res.render('gallery',{images:images});
});
});
2)我强烈建议第三方也这样做。 我目前正在使用异步,但我可能会在将来迁移到承诺。
async.map(
imageIds,
function(eachImageId,next){
Images.findById(eachImageId).exec(function(findImageErr, foundImage) {
next(null,foundImage);
// don't report errors to async, because it will abort
)
},
function(err, images){
images=_.compact(images); // remove null images, i'm using lodash
res.render('gallery',{images:images});
}
);
编辑:关注您的可读性评论,请注意,如果您为< findById(...)创建了一些包装函数.exec(...)'忽略错误并将它们报告为null(称之为' findIgnoreError'(imageId,callback))然后你可以写:
async.map(
imageIds,
findIgnoreError,
function(err, images){
images=_.compact(images); // remove null images, i'm using lodash
res.render('gallery',{images:images});
}
);
换句话说,如果读者开始思考函数,它会变得更具可读性......它说"并行查看那些imageIds,运行" findIgnoreError"在每个imageId上,最后一节说明如何处理累积的结果......
答案 2 :(得分:0)
我没有N次查询mongo(或任何数据库),而是使用$ in激发单个查询:
Images.find({ _id : { $in : imagesIds}},function(err,images){
if(err) return next(err);
res.render('gallery',{images:images});
});
这也会减少io的数量,而且你不必编写其他代码来处理 res.render