前端控制接口并发数量

238 阅读3分钟

需求:多个接口同时调用,控制并发数量,并最终返回请求结果集合

拿到需求之后首先分析,确定函数签名:函数接收两个参数:请求集合以及最大限制数量,返回一个promise对象

function concurrencyApi(apiList, limit) {
    return new Promise()
}

首先简化需求,只看后半句,不由得让人想起promise.all,那么不妨先动手写一个promise.all

Promise.myAll = function (list) {
    return new Promise((resolve, reject) => {
        let count = 0;
        const result = []
        let fulCount = 0
        for (const [index, item] of list.entries()) {
            count++
            Promise.resolve(item).then(ress => {
                result[index] = ress
                fulCount++
                if (fulCount === count) {
                    resolve(result)
                }
            }).catch(err => {
                reject(err)
            })
        }
        if (count === 0) {
            resolve(result)
        }
    })
}

promise.all的思路相对就比较简单了,这里试用for of循环是因为接收的参数不一定是个数组,有可能是个迭代器。

整体思路大概就是每一项promise执行后,都去检查一下当前的完成数量与list总数是否一致,如果一致代表为最后一次完成,则把整体返回结果resolve出来

那为了使整个逻辑看起来更清晰,这里使用面向对象风格;

既然需求的后半句已经完成了,那我们来尝试完成一下前半句需求:控制并发数量

首先定义constructor,初始化必要的参数

class ConcurrencyApi{
    constructor(apiList, limit) {
        this.apiList = apiList
        this.limit = limit
    }
}

那下一步,控制并发数量,那这里如果相对异步的流程随心所欲,生成器是再好不过的工具了

class ConcurrencyApi{
     constructor(apiList, limit) {
        this.apiList = apiList
        this.limit = limit
        this.generator = this.getApiItem()
    }
    ...
    
    *getApiItem() {
        for (let index = 0; index < this.apiList.length; index++) {
            const element = this.apiList[index];
            yield {
                api: element,
                key: index
            }
        }
    }
}

那我们可以想象,如果限制数量为一条,那么其实就是N个接口依次执行,那首先就想到了递归调用~

那我们就来写一下需要递归的函数

class ConcurrencyApi{
    ...
    
    runApi() {
        const {value, done} = this.generator.next()
        if(done) {
            return
        }
        value.api().then(async res => {
            this.resList[value.key] = await res.text() // 这里使用的是fetch,需求对res二次解析
            this.runApi()
        })
    }
}

那如果runApi()这个function只执行一遍,就完成了我们的依次调用,执行一遍就相当于一条赛道,那如果我们想再多个赛道进行接力赛跑,那在首次的时候,我们调用limit次runApi(),就可以得到limit条赛道了,也就是需求中的限制最大并发数

class ConcurrencyApi{
    ...
    
    init() {
        Array(this.limit).fill(1).forEach((item, index) => {
            this.runApi()
        })
    }
}

那到这里为止,并发数量已经限制住了,剩下的就是收集返回结果,思路就参考上面的promise.all的实现思路

class ConcurrencyApi{
    constructor(apiList, limit) {
        this.apiList = apiList
        this.limit = limit
        this.generator = this.getApiItem()
        this.resList = []
        this.getAllresolve = null
    }
    ...
    
    all(res, index) {
        this.init()
        return new Promise(resolve => {
            this.getAllresolve = resolve
        })
    }
}

首先定义一个all的方法,并且执行init()方法,把resolve给记录下来,等待所有结果并返回

class ConcurrencyApi{
    ...
    
    runApi() {
        const {value, done} = this.generator.next()
        if(this.resList.length === this.apiList.length) {
            let everyHaveValue = true
            for(let i = this.apiList.length - 1; i >= 0; i--) {
                if(!this.resList[i]){
                    everyHaveValue = false
                }
            }
            if(everyHaveValue) {
                this.getAllresolve(this.resList)
            }
        }
        if(done) {
            return
        }
        value.api().then(async res => {
            this.resList[value.key] = await res.text()
            this.runApi()
        })
    }
}

然后每次执行runapi方法时,通过判定this.resList.length === this.apiList.length是否执行完毕,但注意的是,因为结果需要按顺序收集,所以很可能最后一项的返回结果优先出,那因为通过arr[index]去赋值,resList很大概率成为一个稀疏数组,这里就简单的通过判断每一项是否存在来判断是否全部完成(不够严谨,根据实际业务调整),注意的是,arr.some/arr.every等方法会忽略掉稀疏数组的稀疏项,所以在这里使用some或者every是无效的。

整体代码如下

class ConcurrencyApi{
        constructor(apiList, limit) {
            this.apiList = apiList
            this.limit = limit
            this.generator = this.#getApiItem()
            this.resList = []
            this.getAllresolve = null
        }

        *#getApiItem() {
            for (let index = 0; index < this.apiList.length; index++) {
                const element = this.apiList[index];
                yield {
                    api: element,
                    key: index
                }
            }
        }

        #init() {
            Array(this.limit).fill(1).forEach((item, index) => {
                this.#runApi()
            })
        }

        #runApi() {
            const {value, done} = this.generator.next()
            if(this.resList.length === this.apiList.length) {
                let everyHaveValue = true
                for(let i = this.apiList.length - 1; i >= 0; i--) {
                    if(!this.resList[i]){
                        everyHaveValue = false
                    }
                }
                if(everyHaveValue) {
                    this.getAllresolve(this.resList)
                }
            }
            if(done) {
                return
            }
            value.api().then(async res => {
                this.resList[value.key] = await res.text()
                return this.#runApi()
            })
        }

        all(res, index) {
            this.#init()
            return new Promise(resolve => {
                this.getAllresolve = resolve
            })
        }
    }

具体使用

const task = new ConcurrencyApi(list, 3)

task.all().then(res => {
})