利用「占位组件」实现请求「并发控制」

457 阅读6分钟

各位好,我是李仲轩,这篇文章是农历新年后的第一篇,这一篇来讲一个实际工作遇到的关于控制并发的小需求;以后遇到相同的情况,可以参考本篇文章。PS:想直接看效果的,可以往下翻到「最终效果」进行查看

需求

这个需求是这样的,我有一个查询的表格,里面每一行的一个字段值,需要从另一个接口取值取然后赋值上去,意思就是:

image.png

我要先请求这个列表的数据,然后依次或者是批量请求另一个接口,获取到数据,然后赋值上去(示例中就是赋值给表头3代表的字段)。

但是这接口有两个特点:

  • 通过这行数据的id值进行查询,意思是你要查询,你要先取出这一行的id
  • 这个接口有限制,不能一次性请求太多次,容易崩;并且实际测试这个接口发现它的并发容量是7,超过这个数字就会崩

那么问题就来了

  • 每一页需要展示的数据量超过了并发容量数,并且并发请求有限制
  • 如何在不用并发请求的方式如何把数据获取到并且更新呢?

思维链

认真分析之后,发现;解决这个需求,主要有两个关键点:

  • 控制并发请求
  • 优化获取数据并赋值的流程

首先第一点就是「控制并发请求」,这里的相关代码,我贴在后面,我先说思路。

控制并发请求的点有以下几个:

  • 请求池:把要触发的请求收集过来,不让它马上就触发
  • 容量:设置并发请求数量,比如1、2、3(ps:本次例子容量为1)
  • 启动:找一个契机按照容量进行启动,这个契机是判断到没有新的请求进入请求池了(ps:本次例子使用的防抖来进行的触发)
  • 返回值:返回接口的结果

根据以上四点,可以自己先去实现一下。


第二点就是「优化获取数据并赋值的流程」,接下来我使用思维链的方式来复盘我当时的一个思路:

思考详情顺序
我首先要请求原数据列表,然后再请求第二个接口
如何赋值呢?收集这一页的全部id?,然后通过并发请求控制来获取值,最后根据id,赋值到前面的列表里面去
使用这种方式的话,那么这一页的这个字段岂不是要等到所有流程走完,才会最终显示;效果太差了,pass
并且上述方案还有一个问题,耦合太严重,每次分页和页码、页数变动我都要重新手动处理一遍这样的过程,能不能自动化?,太低效了
ok,全部等完不行,那么我一个一个的请求行不行,请求一个我就显示一个,然后自动处理下一个
如果是这种方案,那么我就需要一个组件当做这个字段的占位符,让这个组件来处理请求到显示的过程
这样再结合上述的并发控制,那不就齐活了吗?我只要设计个规则让这个组件自动动起来,分页事件触发的数据更新,也能保持自动化

总结:让一个占位组件来承担自动化的工作:自动获取数据->调用并发逻辑->赋值数据

最终方案

上面一部分是我自己的思考链路,我这里把最后这个占位符组件的逻辑进行图示化

image.png

注意:我这里实际操作的过程中还有一个需要和后端配合的点,那就是让后端把第一个接口中的需要二次获取的字段值约定为空字符串,因为第二个接口明确会返回的是数字,总是会有值的,所以用空串来区分是否有值。


那么最终的方案就是:

  1. 第一个接口中约定待填充字段数据为空字符串,约定以这个字段是否是空串来判断是否有值
  2. 表格中用使用占位符组件在插槽中渲染,把逻辑放到这个组件内部处理
  3. 写一个控制请求并发的类,并导出实例给外部使用
  4. 在组件中处理控制请求逻辑,并发出更新事件,父组件配合处理

那么这样设计之后,即使是分页控件变化导致的重新请求,这个组件也能完全自动化,不用手动去收集id那些了:

// amount 占位符的值
// keyWord 接口返回结构中代表值的key
// req 请求
// amount 自定义事件
<template #amount="{ row }">
    <data-amount
      :amount="row.success_num"
      keyWord="amount"
      :req="getNum(row.task_id)"
      @amount="handleAmount(row, $event)"[jcode](https://code.juejin.cn/pen/7473146706834489355)
    ></data-amount>
</template>

关键代码

控制并发请求类

这里的话,我就贴一个控制并发请求类的一个代码,可以在这个基础上进行优化,这里没有设置阈值,因为我是让它一个接一个走的:

class RequestLimit {
  constructor() {
    // 请求列表
    this.requestList = [];
    // 防抖ID
    this.debounceId = null;
  }

  addRequest(request) {
    return new Promise((resolve, reject) => {
      this.requestList.push({ request, resolve, reject });

      // 防抖,等待此轮添加请求完成之后,再开始执行列表的请求
      clearTimeout(this.debounceId);
      this.debounceId = setTimeout(() => {
        this.executeRequestByList();
      }, 10);
    });
  }

  executeRequestByList() {
    const executeNext = () => {
      if (this.requestList.length === 0) return;

      const item = this.requestList.shift();
      item
        .request()
        .then(item.resolve)
        .catch(item.reject)
        .finally(() => {
          executeNext(); // 递归调用处理下一个请求
        });
    };

    executeNext();
  }
}

export default new RequestLimit();

占位符组件

<template>
  <div :class="['data-amount', { dynamic: !amount }]">
    <i v-if="!amount" class="icon-indicator-icon"></i>
    <span v-else>{{ amount }}</span>
  </div>
</template>

<script setup>
import { defineProps, defineEmits, watch } from "vue";
import RequestLimit from "./index";

const props = defineProps({
  amount: {
    type: String,
    required: true,
    default: ""
  },
  keyWord: {
    type: String,
    required: true
  },
  req: {
    type: Function,
    required: true
  }
});

const emit = defineEmits(["amount"]);

// 使用watch来启动添加请求的操作
watch(
  () => props.amount,
  (newVal) => {
    if (!newVal) {
      getAmount();
    }
  },
  { immediate: true }
);
const getAmount = () => {
  RequestLimit.addRequest(props.req)
    .then((res) => {
      // 发出 "amount" 事件,将结果转为字符串传递给父组件
      emit("amount", res.data[props.keyWord] + "");
    })
    .catch((error) => {
      console.error(error);
    });
};
</script>

<style scoped lang="scss">
@keyframes loading {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
.data-amount {
  display: inline-block; // 保持行内块,方便动画显示
}
.dynamic {
  animation: loading 1s linear infinite;
}
</style>

最终效果

我重新写一份在掘金的代码片段上,请求模拟的2s(实际情况比这个快),这样也能查看效果,比单纯地贴代码有意思(点击运行即可看到效果):

总结

👏 感谢你看到这里,这篇文章介绍的内容是把控制并发和组件结合起来实现自动化请求数据的实际例子,是一个挺有意思的实践;后续各位遇到类似情况,可以参考本文。

最后,如果你喜欢我的文章,或者是它对你有用,点赞、收藏、评论随意给一个就行,这确实是激发创作者创作的动力

我是李仲轩,我们下一篇文章再见吧!👋