异步函数链式调用在很多场景下都会有使用到,举一个最简单的场景:
例如有一个表单,需要上传附件,然后获取到附件的地址,回填给表单,然后再提交表单;
这种场景下因为使用的接口少,所以解决方案有很多,例如直接使用回调函数,或者使用 async/await,或者使用 Promise.then 等等,都可以解决这个问题;
1. 回调函数解决
使用回调函数解决这个问题是非常简单的,也是最初级的解决方案:
js
// 上传文件
function uploadFile(file, callback) {
// some code...
// 上传成功后,调用 callback 函数
callback()
}
// 提交表单
function submitForm(callback) {
// some code...
// 提交成功后,调用 callback 函数
callback()
}
// 使用
uploadFile(file, function() {
submitForm(function () {
// 提交成功后,跳转到列表页
window.location.href = '/list'
})
})
这种方式的缺点是,如果需要调用的函数多了,就会出现回调地狱,代码会变得非常难看,而且不容易维护;
例如上面的例子,只有三层嵌套,如果再多一点就会非常难受了,但是现在应该没有人去使用这种方式了吧;
2. async/await
解决
使用 async/await
解决这个问题也是非常简单的,只需要将上面的代码稍微改造一下就可以了:
js
// 上传文件
function uploadFile(file) {
return new Promise((resolve, reject) => {
// some code...
// 上传成功后,调用 resolve 函数
resolve()
})
}
// 提交表单
function submitForm() {
return new Promise((resolve, reject) => {
// some code...
// 提交成功后,调用 resolve 函数
resolve()
})
}
// 使用
async function main() {
await uploadFile(file)
await submitForm()
// 提交成功后,跳转到列表页
window.location.href = '/list'
}
这种代码看起来就轻松很多,而且也不会出现回调地狱,但是这种方式也有缺点,就是如果需要调用的函数很多,就会出现很多 await
,代码看起来也不是很优雅;
3. Promise.then
解决
使用 Promise.then
解决这个问题其实和 async/await
差不多,同时感觉是结合了回调函数
和 async/await
的优点,代码看起来也比较优雅:
js
// 上传文件
function uploadFile(file) {
return new Promise((resolve, reject) => {
// some code...
// 上传成功后,调用 resolve 函数
resolve()
})
}
// 提交表单
function submitForm() {
return new Promise((resolve, reject) => {
// some code...
// 提交成功后,调用 resolve 函数
resolve()
})
}
// 使用
uploadFile(file).then(() => {
return submitForm()
}).then(() => {
// 提交成功后,跳转到列表页
window.location.href = '/list'
})
这种方式是属于链式调用
,也是一种很火的方式,但是这种方式也有缺点,就是如果需要调用的函数很多,就会一直链下去,一直链下去造成的效果就是,结合了 回调函数
和 async/await
的缺点;
通常这种链式调用的方式在为某种数据进行配置的时候用起来会比较优雅,例如在vue-cli
中对 webpack
进行配置的时候,就是使用这种方式;
js
module.exports = {
chainWebpack: config => {
config
.addRule()
.test(/\.vue/)
.use('vue-loader')
.loader('vue-loader')
.end()
}
}
4. 第三方库解决
现在这种轮子已经很多了,例如async
、rxjs
、bluebird
、q
等等,这些库都很好的解决了这些问题,我上面介绍的这些目前也只有async
和rxjs
还在迭代,其他的很久都没更新了;
那么我就拿async
和rxjs
来举例子,看看他们是如何解决这个问题的;
4.1 async
解决
async
提供了非常丰富的方法,例如async.waterfall
、async.series
、async.parallel
等等,这些方法都是用来解决这种问题的,我这里就拿async.waterfall
来举例子;
js
function asyncFn1() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
}
function asyncFn2() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 1000)
})
}
function asyncFn3() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3)
}, 1000)
})
}
async.mapSeries([asyncFn1, asyncFn2, asyncFn3], (asyncFn, callback) => {
// 依次执行异步操作, 并将结果传递给 callback
asyncFn().then(res => {
// 执行完毕后, 调用 callback 传递结果
callback(null, res);
})
}).then(res => {
console.log(res);
});
async
提供了mapSeries
、mapLimit
、map
等方法,这些方法都是用来解决这种问题的,这里我就拿mapSeries
来举例子;
mapSeries
作用其实和我们常用的Array.prototype.map
差不多,只不过mapSeries
是异步的,他是通过callback
来确定执行下一个函数的,如果callback
不调用,那么就不会执行下一个函数;
除此之外,async
还提供了很多流程控制的方法,例如async.waterfall
、async.series
、async.parallel
等等,这些方案都是用来解决类似的问题的,这里就不一一介绍了;
4.2 rxjs
解决
上面的async
的主要使用场景在nodejs
上,当然他也提供了es
的版本,而rxjs
的主要使用场景在web
端;
他们并不是相同类型的库,他们的核心思想以及解决问题的场景都是不同的,我这里只是举例可以解决这个问题,不要将他们进行对比;
rxjs
也提供了流程控制的方法,例如concat
、merge
、forkJoin
等等,这些方法都是用来解决这种问题的,这里我就拿concat
来举例子;
js
const {of, concatMap} = require('rxjs');
of(asyncFn1, asyncFn2, asyncFn3).pipe(
concatMap(asyncFn => asyncFn())
).subscribe(res => {
console.log(res);
})
使用rxjs
来完成这种操作看起来代码更加简洁,因为rxjs
的核心思想就是数据流
,所以他的代码看起来更加优雅;
这里首先使用of
将三个函数转换成Observable
,然后使用concatMap
来依次执行这三个函数,这里的concatMap
就是rxjs
提供的流程控制方法,他的作用就是依次执行函数;
最后使用subscribe
来订阅这个Observable
,这里的subscribe
就是rxjs
提供的订阅方法,他的作用就是订阅这个Observable
,然后执行这个Observable
;
到这里就不过多的介绍了,如果想要了解更多的rxjs
的知识,可以去看看官方文档;
5. 自己实现
最上头的一块还是自己实现,首先我们可以使用Array.prototype.reduce
来实现这个功能,但是这里有一个问题,就是reduce
是同步的,所以我们需要将他转换成异步的,这里我使用async/await
来实现;
js
async function serialAsync(asyncFnList) {
// 使用 reduce 来实现异步函数链式调用
const res = await asyncFnList.reduce(async (prev, next) => {
// 获取上一个异步函数的结果
const prevRes = await prev
// 执行下一个异步函数
const nextRes = await next()
// 将结果收集起来,然后返回
return [...prevRes, nextRes]
}, Promise.resolve([]))
return res
}
serialAsync([asyncFn1, asyncFn2, asyncFn3]).then(res => {
console.log(res)
})
这里使用reduce
来实现异步函数链式调用,直接将要串行执行的函数列表使用reduce
来串起来,然后使用async/await
来实现异步,最后返回结果;
代码一看其实很简单,但是能想出这样操作的人真他娘的是个天才。
5.1 进阶洋葱模型
上面只是一个简单的串行执行的例子,而在我们常使用的工具中,例如axios
、koa
、express
等等,他们都实现了一个拦截器的功能,这个拦截器内部实现就是一个洋葱模型;
而这个洋葱模型就是一个异步函数链式调用,这里我就拿axios
来举例子;
js
const axios = require('axios')
function seelp(time) {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, time)
})
}
axios.interceptors.request.use(async config => {
await new seelp(1000)
console.log('request1')
return config
})
axios.interceptors.request.use(async config => {
await new seelp(1000)
console.log('request2')
return config
})
axios.interceptors.request.use(async config => {
await new seelp(1000)
console.log('request3')
return config
})
这里的axios.interceptors.request.use
就是axios
提供的拦截器,他的作用就是拦截请求,然后执行拦截器内部的函数;
在使用的过程中,我们可以注册异步函数,但是他一样是按照顺序执行的,这里就是一个洋葱模型,他的执行顺序是request1
-> request2
-> request3
;
他是怎么实现的我们不管,但是我们可以自己实现一个类似的功能:
js
class Onion {
constructor() {
this.middlewares = []
}
// 调用 use 方法注册中间件
use(fn) {
this.middlewares.push(fn)
}
// 调用 run 方法执行中间件
async run() {
// 获取中间件列表
const middlewares = this.middlewares
const len = middlewares.length
// 定义 next 函数
let next = async () => {
return Promise.resolve()
}
// 从后往前依次执行中间件
for (let i = len - 1; i >= 0; i--) {
// 获取当前需要执行的中间件
const currentFn = middlewares[i]
// 将 next 函数传递给当前中间件
const nextFn = next
// 重新定义 next 函数,让他执行当前中间件
next = async () => {
// 执行当前中间件,并将 next 函数传递给当前中间件
await currentFn(nextFn)
}
}
// 当循环结束后,执行 next 函数,这个时候 next 函数就是第一个中间件
await next()
}
}
const onion = new Onion()
onion.use(async next => {
await seelp(1000)
console.log('request1')
next()
})
onion.use(async next => {
await seelp(1000)
console.log('request2')
next()
})
onion.use(async next => {
await seelp(1000)
console.log('request3')
next()
})
onion.run()
洋葱模型的核心思想就是next
函数,他的作用就是执行下一个中间件,这里的next
函数其实就是一个异步函数链式调用,代码虽然很少,但是非常值得仔细品味;
6. 总结
异步函数串行执行,并行执行在很多场景下都会使用到,今天带来的是一个异步函数串行执行的方案,这个方案其实是一个非常通用的方案,可以用在很多场景下;
我们从最开始的问题产生,到最后的问题解决,提供了很多的解决方案,这些方案都是非常优秀的,但是他们都有各自的缺点,所以我们需要根据实际的场景来选择;
最后我还提供了两个自己实现的方案,一个是简单的异步函数串行执行,一个是进阶的洋葱模型,这两个方案都是非常通用的,可以用在很多场景下;
希望这篇文章能够帮助到你,如果你有更好的方案,欢迎留言讨论;