koa如何用async/await解决express无法处理异步函数的问题
最近在看express
和 koa
时,对 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
决定的呢?
看下打印结果:
可以看到,在7秒之前result
的promise
状态全部都是pengding
,之后就变成了fulfilled
,所以result
的promise
状态是由最后一个then
里面的promise
的状态决定的。
先记住这一点,koa
中间原理就是利用了这一点,后面会讲到。
Promise.resolve
接着看下Promise.resolve
的使用,它返回一个执行完成之后的promise
,如图所示:
同样的道理,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)
可以看到,result1
的promise
状态也是由最后一个promise
的状态决定的。
async/await
最后来看下async/await
的使用。
async
用来声明一个函数是一个异步函数,它的返回值是一个promise
。
既然返回值是一个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
中间件原理
关于express
和koa
中间件的实现,可以查看我写的这篇文章对于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
如果你看懂上面的中间件执行逻辑,那么就应该能理解这里的打印顺序。
- 执行第一个中间件,打印
I am the first middleware
- 执行
next
函数,即执行第二个中间件,打印I am the second middleware
- 执行
next
函数,即执行第三个中间件,打印I am the router middleware => /api/test
- 执行
res.status(200).send('hello')
,本次请求结束,但是后面的代码仍然会打印,即the router middleware end
- 第三个中间件结束后,执行第二个中间件
next
之后的代码,打印second middleware end calling
- 执行第一个中间件
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