filter操作中使用async/await未等待异步执行完成的问题排查
你遇到的核心问题是Array.prototype.filter 不支持异步回调函数,这也是为什么你看到filter提前退出、HTTP响应提前发送的原因,我来给你拆解清楚:
为什么当前代码会出问题?
当你在filter里传入async回调时,每个回调都会返回一个Promise,而JavaScript里所有Promise对象都是真值(truthy)——也就是说,filter会把所有标签都保留下来,根本起不到过滤作用。更糟的是,这些异步操作(getDrivers()、getTrucks()等)会在后台异步执行,而filter会立刻返回一个由Promise组成的数组,后续的代码(比如// #6的逻辑)会直接执行,甚至提前给客户端发送HTTP响应,等异步操作完成时,已经没人关心结果了。
你尝试用Promise.all包裹filter的结果,其实是把一堆Promise变成了布尔值数组,但这时候filteredTags存的是每个标签是否符合条件的布尔值,不是原标签对象,后续的map自然也无法正确处理。
正确的实现方式
我们需要先把所有标签的异步校验逻辑完成,得到每个标签是否符合过滤条件的结果,再用这个结果去筛选原标签数组。推荐两种写法:
写法一:用 map + Promise.all + filter
先把每个标签映射为一个Promise,这个Promise返回标签本身和它是否符合条件的标识,再统一等待所有异步操作完成,最后过滤出符合条件的标签:
const getAllTags = async (pureTags = false, unused = true, enabled = true, notRevoked = false) => { const allTags = await db.dbRfid.findAll(); // 先为每个标签生成异步校验的Promise const tagValidationPromises = allTags.map(async (tag) => { let isValid = true; // 校验是否未被使用 if (unused) { const driverTags = await tag.getDrivers(); const truckTags = await tag.getTrucks(); const userTags = await tag.getUsers(); isValid = driverTags.length === 0 && truckTags.length === 0 && userTags.length === 0; } // 校验是否启用 if (enabled && !tag.enabled) { isValid = false; } // 校验是否未被撤销 if (notRevoked && !tag.revoked) { isValid = false; } return { tag, isValid }; }); // 等待所有校验完成 const validatedTags = await Promise.all(tagValidationPromises); // 过滤出符合条件的标签 const filteredTags = validatedTags.filter(item => item.isValid).map(item => item.tag); // 返回处理后的结果 if (pureTags) { return filteredTags; } return filteredTags.map(tag => ({ id: tag.id, rfid: tag.rfid, expiryDate: tag.expiryDate, revoked: tag.revoked, enabled: tag.enabled })); };
写法二:用 for...of 循环(更直观,适合分步调试)
如果你觉得上面的写法有点绕,用for...of循环逐个处理标签也是很清晰的选择,能更直观地控制异步流程:
const getAllTags = async (pureTags = false, unused = true, enabled = true, notRevoked = false) => { const allTags = await db.dbRfid.findAll(); const filteredTags = []; for (const tag of allTags) { let isValid = true; // 校验是否未被使用 if (unused) { const driverTags = await tag.getDrivers(); const truckTags = await tag.getTrucks(); const userTags = await tag.getUsers(); isValid = driverTags.length === 0 && truckTags.length === 0 && userTags.length === 0; } // 校验是否启用 if (enabled && !tag.enabled) { isValid = false; } // 校验是否未被撤销 if (notRevoked && !tag.revoked) { isValid = false; } if (isValid) { filteredTags.push(tag); } } // 返回处理后的结果 if (pureTags) { return filteredTags; } return filteredTags.map(tag => ({ id: tag.id, rfid: tag.rfid, expiryDate: tag.expiryDate, revoked: tag.revoked, enabled: tag.enabled })); };
额外优化建议
如果你的数据库支持关联查询,其实可以在db.dbRfid.findAll()的时候就通过Sequelize的include和where条件直接过滤出符合要求的标签,这样能把大部分逻辑放到数据库层面处理,性能会比在代码里逐个查询关联表好很多。比如:
const getAllTags = async (pureTags = false, unused = true, enabled = true, notRevoked = false) => { const whereConditions = {}; if (enabled) { whereConditions.enabled = true; } if (notRevoked) { whereConditions.revoked = false; } const includeOptions = []; if (unused) { // 通过关联查询的方式,筛选出没有关联司机、卡车、用户的标签 includeOptions.push( { model: db.Driver, required: false }, { model: db.Truck, required: false }, { model: db.User, required: false } ); whereConditions['$Driver.id$'] = null; whereConditions['$Truck.id$'] = null; whereConditions['$User.id$'] = null; } const filteredTags = await db.dbRfid.findAll({ where: whereConditions, include: includeOptions, // 如果不需要关联数据,可以加上这个,减少返回的数据量 attributes: ['id', 'rfid', 'expiryDate', 'revoked', 'enabled'] }); if (pureTags) { return filteredTags; } // 因为已经指定了attributes,这里可以直接返回,或者按需处理 return filteredTags.map(tag => tag.toJSON()); };
这种方式能大大减少数据库查询次数,提升接口性能,推荐优先考虑。
内容的提问来源于stack exchange,提问作者CybeX

