7种异步函数全串行调用方式,高效编码,放弃屎山

异步函数链式调用在很多场景下都会有使用到,举一个最简单的场景:

例如有一个表单,需要上传附件,然后获取到附件的地址,回填给表单,然后再提交表单;

这种场景下因为使用的接口少,所以解决方案有很多,例如直接使用回调函数,或者使用 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. 第三方库解决

现在这种轮子已经很多了,例如asyncrxjsbluebirdq等等,这些库都很好的解决了这些问题,我上面介绍的这些目前也只有asyncrxjs还在迭代,其他的很久都没更新了;

那么我就拿asyncrxjs来举例子,看看他们是如何解决这个问题的;

4.1 async 解决

async提供了非常丰富的方法,例如async.waterfallasync.seriesasync.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提供了mapSeriesmapLimitmap等方法,这些方法都是用来解决这种问题的,这里我就拿mapSeries来举例子;

mapSeries作用其实和我们常用的Array.prototype.map差不多,只不过mapSeries是异步的,他是通过callback来确定执行下一个函数的,如果callback不调用,那么就不会执行下一个函数;

除此之外,async还提供了很多流程控制的方法,例如async.waterfallasync.seriesasync.parallel等等,这些方案都是用来解决类似的问题的,这里就不一一介绍了;

4.2 rxjs 解决

上面的async的主要使用场景在nodejs上,当然他也提供了es的版本,而rxjs的主要使用场景在web端;

他们并不是相同类型的库,他们的核心思想以及解决问题的场景都是不同的,我这里只是举例可以解决这个问题,不要将他们进行对比;

rxjs也提供了流程控制的方法,例如concatmergeforkJoin等等,这些方法都是用来解决这种问题的,这里我就拿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 进阶洋葱模型

上面只是一个简单的串行执行的例子,而在我们常使用的工具中,例如axioskoaexpress等等,他们都实现了一个拦截器的功能,这个拦截器内部实现就是一个洋葱模型;

而这个洋葱模型就是一个异步函数链式调用,这里我就拿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. 总结

异步函数串行执行,并行执行在很多场景下都会使用到,今天带来的是一个异步函数串行执行的方案,这个方案其实是一个非常通用的方案,可以用在很多场景下;

我们从最开始的问题产生,到最后的问题解决,提供了很多的解决方案,这些方案都是非常优秀的,但是他们都有各自的缺点,所以我们需要根据实际的场景来选择;

最后我还提供了两个自己实现的方案,一个是简单的异步函数串行执行,一个是进阶的洋葱模型,这两个方案都是非常通用的,可以用在很多场景下;

希望这篇文章能够帮助到你,如果你有更好的方案,欢迎留言讨论;

相关推荐
腾讯TNTWeb前端团队5 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰9 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪9 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪9 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy10 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom10 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom10 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom10 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom11 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom11 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试