高性能分组列表设计(1)

July 28, 2021
project

高性能分组列表设计

整体目标

  1. 分组存在嵌套关系,且深度无理论上限
  2. 可以通过拖拽,将已分组元素拖出,接触分组关系
  3. 可以通过拖拽,将未分组元素拖入分组内,建立新的分组关系
  4. 未分组列表项移动时,会自动越过分组及其子组件
  5. 未分组列表项进行分组时,应保持分组前的相对顺序
  6. 已分组列表项,在解除分组时,应保持分组前的相对顺序
  7. 以上操作对直接操作分组时,也应有效(这里将分组也作为一个列表项进行操作)

分析

  • 由于目标1&5,数据结构应保持一维结构,即对象数组的形式。这样的数据结构,提供了列表项的基础顺序,方便在创建分组时保持列表项的相对顺序。
  • 对于目标2&3&5&6,在计算拖拽项是否建立/更新/删除分组关系时,应记录已分组列表项在组内的相对位置,方便在分组关系变化时,对列表的位置进行排序
  • 对于目标7,应把分组也作为列表项之一。提供“type”字段作为分组列表项和其他列表的区别,为后续可能拓展分组的展开/收起功能
  • 渲染列表时会使用一个多维结构的数据,方便递归的对列表进行渲染,对于jsx语法友好。

数据结构设计

列表项数据结构

typescript
interface ListItem {
  code: string;
  groupCode: string;
}

列表数据结构

typescript
type List = ListImte[]

更新分组时的辅助数据结构

typescript
type GroupStack = {
  groupCode: string;
  index: number; // 分组真实的下标
  offsetNumber: number // 分组的长度,方便记录分组内列表项的相对位置
}[]

用于react渲染的数据结构

typescript
interface AssistStruct {
  code: string;
  children?: AssistStruct[];
  parentGroupCode?: string; //pop stack flag
}

算法选择

一维对象数组转换成嵌套结构设计:

检测分组闭合,算法属于括号闭合算法的变种。

使用栈记录,未闭合的分组code。当前列表项中的group-code字段与栈顶的code不相等时,表示分组闭合,并且弹出当前栈顶元素。

具体实现

一维对象数组转换成嵌套结构实现:

typescript
/**
 * 将一维数组转成多层结构
 * @param compCodes 所有组件的code
 * @param compDatas 所有组件的数据
 * @returns 返回和code相关的嵌套结构、
 */
const subList = (compCodes: string[], compDatas: JDV.State['compDatas']): AssistStruct[] => {
  let groupStack: GroupStack[] = [];
  const resultData: AssistStruct[] = [];

  const stackPop = (groupCode?: string) => {
    let len = groupStack.length - 1;
    while (len >= 0) {
      if (groupStack[len].groupCode !== groupCode) {
        groupStack.pop();
      } else {
        break;
      }
      len--;
    }
  };

  const setResult = (result: AssistStruct[], groupStack: GroupStack[], groupCode: string, value: AssistStruct) => {
    groupStack.forEach((item, index) => {
      if (!result) {
        return null;
      }
      if (!result[item.index]) {
        return;
      }
      if (result[item.index].code !== groupCode) {
        // 如果当前组件的分组不等于结果中的key,向下搜索
        return setResult(result[item.index].children as AssistStruct[], groupStack.slice(index + 1), groupCode, value);
      } else {
        if (result[item.index].children) {
          (result[item.index].children as AssistStruct[]).push(value);
          item.offsetNumber += 1;
        } else {
          result[item.index].children = [value];
        }
      }
    });
  };

  compCodes.forEach((item, index) => {
    const hasGroup = compDatas[item] ? compDatas[item].config.groupCode : undefined;
    stackPop(hasGroup);
    if (compDatas[item].compCode === 'group') {
      if (hasGroup) {
        // 如果当前组件的父组件在栈顶,更新结果树
        setResult(resultData, groupStack.slice(0), hasGroup, {
          code: item,
          children: [],
        });

        //如果当前分组有父分组,此时分组栈一定不为空,分组索引为父分组长度-1
        // debugger;
        groupStack.push({
          groupCode: item,
          index: groupStack.length ? groupStack[groupStack.length - 1].offsetNumber - 1 : index,
          offsetNumber: 0,
        });
      } else {
        groupStack = []; //没有分组,清空栈
        resultData.push({
          code: item,
          children: [],
        });
        //如果当前分组没有父分组,此时分组栈一定为空,分组索引为结果长度
        groupStack.push({
          groupCode: item,
          index: resultData.length - 1,
          offsetNumber: 0,
        });
      }
    } else {
      if (hasGroup) {
        // 如果当前组件的父组件在栈顶,更新结果树
        setResult(resultData, groupStack.slice(0), hasGroup, {
          code: item,
        });
      } else {
        groupStack = []; //没有分组,清空栈
        resultData.push({
          code: item,
        });
      }
    }
  });
  return resultData;