likes
comments
collection
share

小小的级联选件我遇到了哪些问题项目中有一个下图这样的选择组件需要实现,数据是有层级关系的,需要支持单选多选。 看起来很简

作者站长头像
站长
· 阅读数 20

一、背景

项目中有一个下图这样的选择组件需要实现,数据是有层级关系的,需要支持单选多选。

看起来很简单,我们可以直接复用 antd 的级联组件,已经支持大部分级联组件需要的功能了。

小小的级联选件我遇到了哪些问题项目中有一个下图这样的选择组件需要实现,数据是有层级关系的,需要支持单选多选。 看起来很简

但是在实际应用中,因为数据请求方式的限制,还是遇到了很多问题,一起看看都是怎么解决的。

二、遇到的问题

1. 怎么查数据

级联数据有 10万+ 条,那是一次查所有数据还是一级一级查呢?

项目中有 2 种类型的级联数据,由于上游的限制,类型 1 的级联数据支持一次查所有数据也支持一级一级查,而类型 2 则只支持一级一级查。

而类型 1 的级联数据,必须支持搜索,不然 10万+ 数据没法挨个找。

因此,我们的级联组件要同时支持一次查所有数据和一级一级查这 2 种查数据的模式。

1.1 一次查所有数据

一次查所有就很简单,请求一次接口就行。

  • 适用情况:数据量较小或用户对加载时间要求不高。
  • 优点:前端处理简单,用户体验良好,避免了多次网络请求。
  • 缺点:对数据量较大的情况,可能导致加载时间长、内存占用高,可能影响性能。

1.2 一级一级查数据

一级一级查数据则是先查第一级,这时查到后写死第一级的每一个 childrenisLeaf: false,因为只有 isLeaf 为 false 时,才能继续查下一级,直到查不到。

  • 适用情况:数据量大、层级复杂,或数据支持分级请求。
  • 优点:减少内存占用,初始加载时间较短,逐步加载数据减少了对性能的压力。
  • 缺点:用户体验可能受影响,因为需要逐级加载,增加了请求延迟。

2. 初始化渲染

由于我们的组件要同时支持一次查所有数据和一级一级查这 2 种查数据的模式,导致后面所有的逻辑都要兼容这 2 种数据方式。

初始化渲染,指的是页面详情,组件初始是有选中态的,而这个选中态能在级联组件中渲染出来的条件是,选中了的 id 的数据都已经查到了。

2.1 一次查所有数据

一次查所有的初始化渲染不需要做特别的处理,因为所有数据都有,级联选择器的选中态能正常渲染出来。

2.2 一级一级查数据

一级一级查这种模式,需要根据组件的选中状态,逐步加载数据来渲染选中态。

同时需要处理不同选中模式(单选、多选)的数据请求和渲染逻辑。

3. 级联列表数据渲染

上面已经介绍了,怎么新建时查数据,怎么详情渲染时查数据。

这时我们注意到,组件渲染所需的数据结构和接口返回的数据结构需要互相转换,才能实现正确的数据渲染和选中:

  1. 级联列表数据渲染:组件列表的数据结构 <-> 接口返回的数据结构
  2. 级联选中态数据渲染:组件选中态的数据结构 <-> 接口返回的数据结构

3.1 一次查所有数据

在一次查所有数据的模式下,直接将接口返回的数据结构转换为组件需要的数据结构就行。

3.2 一级一级查数据

一级一级查,根据组件的 value 列表逐级请求数据,并更新组件的树形结构。

3.2.1 获取级联数据

1. 单选

单选时,级联组件的value的数据结构:cat1-cate2-cat3,多级id用-连接。

所以单选就是把 cat1-cate2-cat3转成级联的 id 列表,逐一查询对应级联数据

2. 多选

多选时,级联组件的value的数据结构:string[][],如[['1', '173', '542']]。需要查 1、173的数据,这样级联组件才能正常渲染选中态。

所以多选就是将所有的倒数第一级之外的节点去重后,逐一查询对应级联数据

3.2.2 渲染级联数据

有了每一级的级联数据,怎么更新到树结构中呢。

  1. id 根据 level 排序,小的在前
  2. 先查第一级并更新
  3. 再查剩下的 id,每查到一个,如果能查到这个 id 的 chidren,就找到 tree 中的这个节点,更新这个节点的 chidren,如果查不到,更新这个节点的 isLeaf 为 true

4. 选中态数据渲染

4.1 单选

  • 正常渲染:单选接口返回的数据结构 ->单选组件需要的数据结构
  • 正常选中:单选组件需要的数据结构 ->单选接口返回的数据结构

4.2 多选

  • 正常渲染:多选接口返回的数据结构 ->多选组件需要的数据结构
  • 正常选中:多选组件需要的数据结构 ->多选接口返回的数据结构

5. 单应用有多个级联组件

上面提到,级联数据有 10w+ 条,单应用多个页面存在多个级联组件,我们应该把数据缓存下来,避免每次使用组件都重新加载数据。

5.1 一次查所有数据

一次查所有数据,使用状态管理工具(如 Zustand)存储数据,避免重复请求。

5.2 一级一级查数据

当一个页面用了 5 次级联组件,而且都是多选,每个都选了 5 个数据,每个数据都有 5 层,这时我们通过 id 查对应的级联数据需要请求多少次接口?这其中会不会有 id 重复,如果重复了,数据是不是被覆盖了。

那我们怎么解决这个问题呢?

实现一个RequestCollector 类,用于处理大量请求的防抖和去重。这个类确保了在短时间内对相同的请求进行去重,并按顺序逐个处理。这里的关键点是:

  • 去重:只保留每个 catId 最先出现的请求,避免重复请求。
  • 排序:根据 level 排序确保先处理低级别的请求。
  • 定时处理:使用 setTimeout 延迟处理请求,防止频繁请求。
type RequestFn = (e: { catId: string, level: number }) => Promise<void>

interface RequestItem {
  requestFn: RequestFn
  requestParam: {
    catId: string
    level: number
  }
}

export default class RequestCollector {
  private interval: number
  private queue: RequestItem[]
  private timer: NodeJS.Timeout | null

  constructor(interval = 100) {
    this.interval = interval
    this.queue = []
    this.timer = null
  }

  /**
   * 添加请求到队列
   * @param {RequestItem} item - 请求项,包含 requestFn 和 requestParam
   */
  public addRequest(item: RequestItem): void {
    this.queue.push(item)

    // 如果计时器不存在,则设置计时器
    if (this.timer === null) {
      this.timer = setTimeout(() => this.processRequests(), this.interval)
    }
  }

  /**
   * 处理队列中的请求
   */
  private async processRequests(): Promise<void> {
    // 排序并去重
    const uniqueSortedRequests = this.sortAndDeduplicate(this.queue)

    // 清空队列
    this.queue = []

    // 逐一处理请求
    for (const item of uniqueSortedRequests) {
      const { requestFn, requestParam } = item
      try {
        await requestFn(requestParam)
      } catch (error) {
        console.error('Request failed:', error)
      }
    }

    // 处理完成后重置计时器
    this.timer = null
  }

  /**
   * 根据 requestParam 的 level 对数组进行排序并去重
   * @param {RequestItem[]} requests - 包含 { requestFn, requestParam } 的数组
   * @returns {RequestItem[]} - 排序并去重后的数组
   */
  private sortAndDeduplicate(requests: RequestItem[]): RequestItem[] {
    // 排序:先根据 level 升序排序
    const sorted = requests
      .slice()
      .sort((a, b) => a.requestParam.level - b.requestParam.level)

    // 去重:保留 level 较小的 requestParam
    const unique = sorted.filter(
      (item, index, self) =>
        index ===
        self.findIndex((t) => t.requestParam.catId === item.requestParam.catId)
    )

    return unique
  }
}

可以看到,请求管理器里用到了 setTimeout,setTimeout 里的回调是闭包,因此它是拿不到最新的状态的,所以我们还需要借助一个应用级通用的 Ref 来存储数据。

总结

该组件涵盖了数据获取、渲染、缓存及选中状态管理等多个方面,关键点如下:

  • 数据加载策略:根据数据量和用户需求选择一次性加载所有数据或逐级加载数据的策略。一次性加载适用于数据量小且用户对加载时间不敏感的场景,能够简化前端处理并提高用户体验。但对于数据量大或层级复杂的情况,逐级加载可以节省内存并优化初始加载时间。

  • 初始化渲染:在数据加载和选中态的处理过程中,必须兼顾性能和用户体验。一次性加载的数据可以直接渲染,而逐级加载则需要动态更新组件以适应数据的逐步渲染

  • 数据结构转换:需要确保接口返回的数据与组件所需的数据结构之间能够正确映射,以提高数据处理效率。一次性加载时,数据结构转换较为直接,而逐级加载则需要动态转换和更新,以适应数据的分级结构。

  • 请求管理:通过有效的缓存策略和请求管理器(如 RequestCollector 类)来优化性能。缓存可以减少重复请求并提升响应速度,而请求管理器能有效地去重和防抖,避免不必要的网络请求,确保请求的顺序性和准确性。

通过合理选择数据加载策略、优化初始化渲染流程、准确进行数据结构转换以及高效的请求管理,可以有效地解决复杂的级联选择组件中的各种挑战。实现了在高效的数据处理和优良的用户体验之间的平衡,满足了用户对组件性能和交互的高要求。

转载自:https://juejin.cn/post/7408631611041087528
评论
请登录