情景复现
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');
})
})
- 实际先输出’456’, 然后输出’123’
router.put('/category/:id', (ctx, next) => {
new Promise((resolve) => {
resolve()
}).then(() => {
console.log('456');
ctx.body = { status: 0, message: '修改参数成功234' }
})
})
- 虽然回调方法没有返回Promise, 而且没有async修饰, 但是前端仍然可以收到
ctx.body
的数据
结论
-
为什么问题分析(1)是错的?
- 查看router源码可知, 它并没有判断回调函数是不是Promise, 而是在请求成功或异常的时候, 调用
Promise.resolve(fn())/reject(fn())
, 即调用一次回调函数, 把回调函数的结果放入一个Promise任务
- 查看router源码可知, 它并没有判断回调函数是不是Promise, 而是在请求成功或异常的时候, 调用
-
为什么问题分析(2)是不准确的?
- 因为确实有Promise任务添加了, 但是不是通过回调函数的then()方法
-
为什么在前面的问题解决加个伪字呢?
- 因为那只是解决了表面现象, 并没有体现真实原因, 纵观网上的解决方案, 其实他们的应用场景基本都是
request
或setTimeout
等, 这些触发的异步都是宏任务, 而原生Promise是微任务, 所以产生了执行顺序问题
- 因为那只是解决了表面现象, 并没有体现真实原因, 纵观网上的解决方案, 其实他们的应用场景基本都是
-
为什么解决方法1可行呢?
- 因为Promise.then()又返回了一个Promise, 根据Promise的链式调用执行顺序, 第二个then必然晚于第一个then的执行, 所以ctx.body仍然可以先执行
-
为什么加了
async/await
就可以让setImmediate
先执行呢?- 解决方法2的代码可以用下面的代码来模拟:
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的任务执行顺序问题