Koa-router异步返回ctx.body失效的问题

情景复现

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就可以, 但是为什么呢?

问题分析

  1. router.get()方法的回调函数必须返回Promise, 所以需要显式return new Promise或者为回调方法加async修饰符 [错]
  2. router.get()方法调用回调函数的then()来添加异步任务到微任务队列 [不精确]
  3. ctx.body赋值的执行一定要早于router.get()调用回调函数(这个表述不精确, 请看后面示例中的说明)

问题解决(伪)

根据问题分析, 可以把上面的代码修改为如下几种方式:

  1. 显式返回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错误, 无法找到数据' }
    })
})
  1. 使用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任务
  • 为什么问题分析(2)是不准确的?

    • 因为确实有Promise任务添加了, 但是不是通过回调函数的then()方法
  • 为什么在前面的问题解决加个伪字呢?

    • 因为那只是解决了表面现象, 并没有体现真实原因, 纵观网上的解决方案, 其实他们的应用场景基本都是requestsetTimeout等, 这些触发的异步都是宏任务, 而原生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的任务执行顺序问题

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值