情景复现
router.put('/category/:id', (ctx, next) => {
const data = ctx.request.body
db.updateCategoryById(ctx.params.id, data)
.then((doc) => {
if (doc) ctx.body = { status: 0, message: '修改参数成功' }
else ctx.body = { status: -2, message: 'ID错误, 无法找到数据' }
})
})
上面的例子中, 处理请求时通过mongoose向MongoDB读取数据, 读取方法返回一个Promise, 所以在then()
中为ctx.body
赋值, 返回查询到的数据
存在的问题
实际发送请求, 发现then()
执行了, 但是前端没有收到任何返回的数据
百度结果
照例百度, 基本上都是说加一层Promise就可以, 但是为什么呢?
问题分析
-
router.get()
方法的回调函数必须返回Promise, 所以需要显式return new Promise
或者为回调方法加async
修饰符 [错]
-
router.get()
方法调用回调函数的then()
来添加异步任务到微任务队列 [不精确]
-
ctx.body
赋值的执行一定要早于router.get()
调用回调函数(这个表述不精确, 请看后面示例中的说明)
问题解决(伪)
根据问题分析, 可以把上面的代码修改为如下几种方式:
- 显式返回Promise
router.put('/category/:id', (ctx, next) => {
const data = ctx.request.body
return db.updateCategoryById(ctx.params.id, data)
.then((doc) => {
if (doc) ctx.body = { status: 0, message: '修改参数成功' }
else ctx.body = { status: -2, message: 'ID错误, 无法找到数据' }
})
})
- 使用async/await
router.put('/category/:id', async (ctx) => {
const data = ctx.request.body
try {
const doc = await db.updateCategoryById(ctx.params.id, data)
if (doc) ctx.body = { status: 0, message: '修改参数成功' }
else ctx.body = { status: -2, message: 'ID错误, 无法找到数据' }
} catch (e) {
ctx.body = { status: -1, message }
}
})
实际情况
在本例中, 遇到的是一种特殊情况
虽然mongoose返回的是一个Promise, 但不是原生的Promise, 而是BlueBird.js实现的(官网说性能优于原生Promise), 其内部实现中, 在nodejs环境下, 实际使用的是setImmediate
来触发一个异步任务. 在nodejs中, setImmediate
的执行顺序要晚于原生Promise, 所以就触发了问题分析中的第3条: 执行顺序.
在本例中, 就是router的回调Promise函数先执行完毕(ctx已经发出), 然后执行的mongoose方法返回的"Promise"任务, 而此时ctx已经发出, 所以赋值无效.
下面两段代码也可以证明:
router.put('/category/:id', (ctx, next) => {
const data = ctx.request.body
console.log({res: db.updateCategoryById(ctx.params.id, data)})
db.updateCategoryById(ctx.params.id, data).then((doc) => {
console.log('123');
})
new Promise((resolve) => {
resolve()
}).then(() => {
console.log('456');
})
})
router.put('/category/:id', (ctx, next) => {
new Promise((resolve) => {
resolve()
}).then(() => {
console.log('456');
ctx.body = { status: 0, message: '修改参数成功234' }
})
})
- 虽然回调方法没有返回Promise, 而且没有async修饰, 但是前端仍然可以收到
ctx.body
的数据
结论
new Promise(async (resolve) => {
const a = await new Promise((res) => {
setImmediate(() => {
console.log(123)
res(789)
})
})
console.log(a)
resolve()
}).then(() => console.log(456))
外层Promise是加了async的回调函数, 里面的setImmediate是mongoose的异步方法, 而await让外层Promise的resolve()
在setImmediate的异步返回后才被调用, 这时才真正触发了外层Promise的异步执行(在例子中就是执行ctx.body)
所以, 关键不是Promise的问题, 而是nodejs的任务执行顺序问题