likes
comments
collection
share

koa如何用async/await解决express无法处理异步函数的问题

作者站长头像
站长
· 阅读数 21

最近在看expresskoa时,对 express koa 的中间件执行流程理解有误,于是把其中的关键点拿出来整理成此文。

Promise/async/await

首先来复习下promise/async/await的使用。

Promise

首先看一段代码:

const result = Promise.resolve(
  new Promise(resolve => {
    setTimeout(() => {
      resolve(100)
    }, 3000)
  }).then(res => {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(100)
      }, 3000)
    })
  })
)

console.log('result1', result)

setTimeout(() => {
  console.log('result2', result)
}, 4000)

setTimeout(() => {
  console.log('result3', result)
}, 6000)

setTimeout(() => {
  console.log('result4', result)
}, 7000)

问题:result是一个promise,那它的状态是由哪个then里面的promise决定的呢?

看下打印结果:

koa如何用async/await解决express无法处理异步函数的问题

可以看到,在7秒之前resultpromise状态全部都是pengding,之后就变成了fulfilled,所以resultpromise状态是由最后一个then里面的promise的状态决定的。

先记住这一点,koa中间原理就是利用了这一点,后面会讲到。

Promise.resolve

接着看下Promise.resolve的使用,它返回一个执行完成之后的promise,如图所示:

koa如何用async/await解决express无法处理异步函数的问题

同样的道理,Promise.resolve返回的promise状态也是取决于内部的promise的状态,代码如下:

const result1 = Promise.resolve(
  new Promise(resolve => {
    setTimeout(() => {
      resolve(1)
    }, 2000)
  })
)
console.log(result1) // Promise {<pending>}
setTimeout(() => {
  console.log(result1) // Promise {<fulfilled>: 1}
}, 3000)

可以看到,result1promise状态也是由最后一个promise的状态决定的。

async/await

最后来看下async/await的使用。

async用来声明一个函数是一个异步函数,它的返回值是一个promise

koa如何用async/await解决express无法处理异步函数的问题

既然返回值是一个promise,那么需要使用then才能获取到返回值:

async function getName() {
  const name = 'xiaoming'
  return name
}
getName().then(res => {
  console.log(res) // xiaoming
})
console.log('start')

同时,打印顺序是先打印start,然后再打印xiaoming

除了使用then来获取结果外,我们还可以使用await来获取,await必须在async函数内才能使用。

async function getName() {
  const name = 'xiaoming'
  return name
}

async function getResult() {
  const result = await getName()
  console.log(result) // xiaoming
}

getResult()

你可以把await语句之后的语句看成是then方法第一个回调函数中的语句,即 await getName(); console.log(result); 等同于 getName().then(res => console.log(res))

await关键字左侧返回的值result相当于要传入then方法第一个回调函数的参数res

一般来说,await命令后跟一条返回Promise对象的语句,如果返回的不是Promise对象,则会使用Promise.resolve()包装成Promise对象。

express 中间件原理 到 koa中间件原理

express 中间件原理

关于expresskoa中间件的实现,可以查看我写的这篇文章对于express VS koa 中间件机制的理解

这里只取其中最重要的部分,代码如下:

const next = () => {
  // 获取中间件
  const middleware = stack.shift()
  if (middleware) {
    middleware(req, res, next)
  }
}
next()

我们把所有注册中间件都收集到stact数组中,取出第一个中间件,然后传入函数next,并执行。这段代码如果看不懂,可以多看几遍。

搞懂了上面的代码逻辑,来具体看下express具体执行顺序:

const express = require('express')
const app = express()
const router = express.Router()

const sleep = seconds =>
  new Promise(reslove => {
    setTimeout(() => {
      console.log('sleep timeout...')
      reslove()
    }, seconds)
  })

app.use(function (req, res, next) {
  console.log('I am the first middleware')
  const startTime = Date.now()
  next()
  const cost = Date.now() - startTime
  console.log(`first middleware end calling - ${cost} ms`)
})

app.use(function (req, res, next) {
  console.log('I am the second middleware')
  next()
  console.log('second middleware end calling')
})

router.get('/test', async function (req, res, next) {
  console.log('I am the router middleware => /api/test')
  res.status(200).send('hello')
  console.log('the router middleware end')
})

// 路由中间件挂载
app.use('/api', router)

app.listen('3001', () => {
  console.log('server run at 3001')
})

访问http://localhost:3001/api/test, 打印顺序如下:

I am the first middleware
I am the second middleware
I am the router middleware => /api/test
the router middleware end
second middleware end calling
first middleware end calling - 2 ms

如果你看懂上面的中间件执行逻辑,那么就应该能理解这里的打印顺序。

  1. 执行第一个中间件,打印I am the first middleware
  2. 执行next函数,即执行第二个中间件,打印I am the second middleware
  3. 执行next函数,即执行第三个中间件,打印I am the router middleware => /api/test
  4. 执行res.status(200).send('hello'),本次请求结束,但是后面的代码仍然会打印,即the router middleware end
  5. 第三个中间件结束后,执行第二个中间件next之后的代码,打印second middleware end calling
  6. 执行第一个中间件next之后的代码,打印first middleware end calling - 2 ms,本次所有中间件执行完毕。

express 无法处理异步函数

上面所有的代码都是同步的,因此,打印顺序是比较清晰的,那如果把第三个中间件改变下,如下:

router.get('/test', async function (req, res, next) {
  console.log('I am the router middleware => /api/test')
  await sleep(2000)
  res.status(200).send('hello')
  console.log('end the router middleware')
})

先在脑海里执行下,看看你能否得出打印顺序。

不卖关子了,直接上结果:

I am the first middleware
I am the second middleware
I am the router middleware => /api/test
second middleware end calling
first middleware end calling - 2 ms
sleep timeout...
end the router middleware

当执行到第三个中间件的时候,发现是一个异步函数await sleep(2000),则跳过后面的代码,执行执行第二个中间件next之后的代码,打印second middleware end calling,然后在执行第一个中间件next之后的代码,打印first middleware end calling - 2 ms

等待2s后,执行res.status(200).send('hello'),并打印end the router middleware

这样破坏了洋葱圈的模型,为什么会这样呢?

其实这也很好理解,我们用一段简单的代码来说明下

function start() {
  async function test() {
    async function next() {
      console.log('1')
      await sleep(2000)
      console.log('1-1')
      return 1
    }
    const result = await next()
    console.log('结果', result)
  }
  test()
  console.log('我先执行了')
}
start()

打印顺序是:

1
我先执行了
1-1
结果 1

因为test是一个异步函数,所以直接跳过,执行后面的语句,打印我先执行了

同样的道理,express的中间件也是一个函数嵌套一个函数串行执行的,当遇到异步时,就直接跳过执行后面的语句,所以就会出现上面的执行顺序。

那如何让它能等待异步函数执行完之后,再执行后面的语句呢?我们给它加上async/await

async function start() {
  async function test() {
    async function next() {
      console.log('1')
      await sleep(2000)
      console.log('1-1')
      return 1
    }
    const result = await next()
    console.log('结果', result)
  }
  await test()
  console.log('我最后执行了')
}
start()

打印顺序:先打印1,然后等待2秒后,打印后面的内容。

1
sleep timeout...
1-1
结果 1
我最后执行了

改写express中间件原理

有了上面的基础,现在我们也想把express的中间件机制也改下,即遇到异步函数,等待它执行完之后,再执行后面的语句。

这就是koa要解决的事情

那如何做呢?先看下这段代码:

async function get(next) {
  console.log('1-')
  await next()
  console.log('1-1')
}

async function get1(next) {
  console.log('2-')
  await next()
  console.log('2-2')
}

async function get2(next) {
  console.log('3-')
  await sleep(2000)
  console.log('3-3')
}

const stack = [get, get1, get2]
const next = () => {
  // 拿到第一个中间件
  const middleware = stack.shift()
  if (middleware) {
    middleware(next)
  }
}
next()

执行顺序:

1-
2-
3-
2-2
1-1
sleep timeout...
3-3

这和express中间件的执行顺序一致,那我们就用这个来模拟express的中间件原理,并对它进行改进。

首先我们给next函数加上async,然后把middleware(next)改写为return middleware(next)

const next = async () => {
  const middleware = stack.shift()
  if (middleware) {
    return middleware(next)
  }
}

async function start() {
  await next()
}
start()

这里为什么一定要加上return呢?还记得我们在文章开头讲的内容吗?

async异步函数的返回值promise的状态取决于最后一个then里面的promise的状态。

当执行第一个中间件时,执行await next()后,那await next()什么时候结束呢?也就是这个promise的状态什么时候变为fulfilled呢?这取决于后面中间件的执行结果,因此需要return一个promise出来,同时,它也就一直会等待,等待后面中间件执行完成

这样中间件的执行顺序就变成了洋葱圈模型。

如果,某个中间件是一个普通函数,那么需要使用Promise.resolve包裹下:

const next = async () => {
  const middleware = stack.shift()
  if (middleware) {
    return Promise.resolve(middleware(next))
  }
}

当这样改写后,我们发现这其实就是koa的中间件原理,我们看看koa中间件代码长什么样:

function compose(middlewareList) {
  return function (ctx) {
    function dispatch(i) {
      const fn = middlewareList[i]
      try {
        return Promise.resolve(
          fn(ctx, dispatch.bind(null, i + 1))
        )
      } catch (err) {
        return Promise.reject(err)
      }
    }
    dispatch(0)
  }
}

有没有发现,这和我们改写的代码几乎一模一样。

至此,文章就结束了,也非常感谢你阅读到这里。其实本文的中心点很简单,就是koa如何用async/await解决express无法处理异步函数的问题。

转载自:https://juejin.cn/post/7249532712825536569
评论
请登录