NodeJS for循环嵌套Promise异步赋值异常问题求助
问题:异步循环导致菜单分类的菜品列表为空怎么解决?
问题背景
我现在要实现一个接口,逻辑是先获取菜单分类列表,再遍历每个分类调用接口获取对应菜品,最终返回包含菜品的完整菜单数据。原本获取分类的逻辑是正常的,而且我用注释里的测试代码验证过单个分类调用菜品接口是可行的,但添加循环之后,返回结果里每个分类的itemsList始终是空的。
我的代码
'use strict'; var mongoose = require('mongoose'), MenuCategory = mongoose.model('MenuCategory'); module.exports = function(menuItemController) { var mod = { listEntireMenu(req, res) { return new Promise(function(resolve, reject) { var entireMenu = []; MenuCategory.find({}, function(err, menuCategories) { if (err) { return reject(err) } else { //--------------------------- // 验证调用其他控制器可行的测试代码 //--------------------------- // // var categoryWithItems = menuCategories[0].toObject(); // req.body.menuCategoryID = categoryWithItems._id; // menuItemController.listAllMenuItemsByCategory(req, res).then((menuItems) => { // if(menuItems) // { // return resolve(menuItems); // } // else // { // return { success: false } // } // }); //----------------------------- for (var i = 0; i < menuCategories.length; i++) { var categoryWithItems = menuCategories[i].toObject(); var subItems = []; req.body.menuCategoryID = categoryWithItems._id; menuItemController.listAllMenuItemsByCategory(req, res).then((menuItems) => { if(menuItems) { subItems = menuItems; } else { return { success: false } } }); categoryWithItems.tester = { "itemsList" : subItems }; entireMenu.push(categoryWithItems); } return resolve(entireMenu) } }); }).then((menuCategories) => { if(menuCategories) { return menuCategories } else { return { success: false } } }); }, } return mod; };
返回结果
[ { "_id": "5ed16fxxxxxxxx95676e37", "locationID": "5ed16xxxxxxxx7295676e36", "menuCategoryName": "Category One", "Created_date": "2020-05-29T20:26:34.991Z", "__v": 0, "tester": { "itemsList": [] } }, { "_id": "5ed170xxxxxx95676e38", "locationID": "5ed16xxxxxxxx7295676e36", "menuCategoryName": "Category Two", "Created_date": "2020-05-29T20:26:48.799Z", "__v": 0, "tester": { "itemsList": [] } } ]
路由调用代码(route.js)
app.get('/api/listEntireMenu', (req, res) => { menuCategoryController.listEntireMenu(req, res).then(menuCategories => res.json(menuCategories)); })
问题分析与解决方案
没错,这绝对是异步操作的时序问题搞的鬼!你现在的代码逻辑里,menuItemController.listAllMenuItemsByCategory是异步的Promise调用,但你的循环完全没等这些异步请求返回结果,就直接把空的subItems塞进分类对象,然后把整个数组resolve出去了。等异步请求真的拿到菜品数据的时候,你的接口早就把空列表返回给前端了,自然看不到菜品。
而且你用var i还有变量作用域的坑——所有异步回调会共享同一个i变量,搞不好最后所有请求都用了最后一个分类的ID,不过核心问题还是没处理好异步等待。下面给你两个实用的解决方案:
方案一:用async/await(最简洁易读)
改成async函数,就能像写同步代码一样处理异步逻辑,可读性拉满:
'use strict'; var mongoose = require('mongoose'), MenuCategory = mongoose.model('MenuCategory'); module.exports = function(menuItemController) { var mod = { async listEntireMenu(req, res) { try { // 先获取分类列表,用exec()返回Promise更规范 const menuCategories = await MenuCategory.find({}).exec(); const entireMenu = []; // 用for...of循环逐个处理分类,await会等待每个请求完成 for (const category of menuCategories) { const categoryWithItems = category.toObject(); req.body.menuCategoryID = categoryWithItems._id; // 等菜品接口返回结果再往下走 const menuItems = await menuItemController.listAllMenuItemsByCategory(req, res); // 给分类加上菜品列表,空的话就设成[] categoryWithItems.tester = { itemsList: menuItems || [] }; entireMenu.push(categoryWithItems); } return entireMenu; } catch (err) { // 捕获并抛出错误,让上层处理 throw err; } }, } return mod; };
方案二:用Promise.all(并行处理,效率更高)
如果各个分类的菜品请求之间没有依赖,用Promise.all同时发起所有请求,比逐个等待快很多:
'use strict'; var mongoose = require('mongoose'), MenuCategory = mongoose.model('MenuCategory'); module.exports = function(menuItemController) { var mod = { listEntireMenu(req, res) { // 先获取分类列表 return MenuCategory.find({}).exec() .then(menuCategories => { // 把每个分类转换成一个获取菜品的Promise const categoryPromises = menuCategories.map(category => { const categoryWithItems = category.toObject(); req.body.menuCategoryID = categoryWithItems._id; // 每个Promise完成后,给分类加上菜品列表再返回 return menuItemController.listAllMenuItemsByCategory(req, res) .then(menuItems => { categoryWithItems.tester = { itemsList: menuItems || [] }; return categoryWithItems; }); }); // 等待所有Promise都完成,返回完整的菜单数组 return Promise.all(categoryPromises); }) .catch(err => { throw err; }); }, } return mod; };
额外小贴士
- 把
MenuCategory.find({}, callback)改成MenuCategory.find({}).exec(),这样返回的是Promise,和后面的异步逻辑更统一,也符合现代Node.js的最佳实践。 - 原来用
var i的循环会有变量泄漏的问题,改成for...of或者let i就能避免,确保每个异步请求用的是当前循环的分类ID。 - 处理
menuItems为空的情况时,直接赋值空数组,避免前端拿到undefined出问题。
内容的提问来源于stack exchange,提问作者Marc Blazyk
相关产品推荐
相关产品推荐

