乐趣区

关于前端:我将antdesign中可搜索的树形控件平均渲染时间减少90

前言

就用户体验感来说说,以下控件起源and-design

  • 给定一个 树形控件
  • 输入框控件,可含糊查问树形数据
  • 查问到数据后开展与之相干的节点

总体来说就是一个可查问的树组件,那么咱们在中后盾业务中,这应该是必不可缺的一个业务吧。

那么再来看看问题是什么呢

如果给定 树形控件 再给 1w 条数据会呈现什么状况,3k 条数据会有什么状况。

首先想到的就是数据量很大,那么必定是会对渲染工夫会造成影响

  1. 含糊查问所用工夫
  2. 查问到指标节点后与之相干的节点(父节点与祖父节点)开展时所耗时间

那么大家对此有什么好的解决方案呢?能够一起分享下

下边呢,写下我的解决办法。因为呢本文在加入金石打算,如果感觉此文对您略微有些帮忙的话请点个赞反对一下

测试数据

先来测试一下 一万多条数据 通过含糊查问以及开展与之相干节点须要耗时多久

简略看看测试数据,对测试数据做个理解

1wtest.js

该文件里放了一万多条测试数据

下列是我将数据扁平化之后的条数,足有 18402 条测试数据

代码构造

看下整体代码构造

html 构造

<template>
  <div>
    <Input
      v-model:value="searchValue"
      style="margin: 0 5px 8px; width: 250px"
      placeholder="Search"
    />
    <Button @click="search" type="primary"> 查问 </Button>
    <Tree
      :expandedKeys="expandedKeys"
      :tree-data="treeData"
      :replace-fields="replaceFields"
      @expand="onExpand"
    >
      <template #title="{label}">
        <span v-if="label.indexOf(searchValue) > -1">
          {{label.substr(0, label.indexOf(searchValue)) }}
          <span style="color: #f50">{{searchValue}}</span>
          {{label.substr(label.indexOf(searchValue) + searchValue.length) }}
        </span>
        <span v-else>{{label}}</span>
      </template>
    </Tree>
  </div>
</template>

javascript 构造

<script lang="ts">
  import {Tree, Input, Button} from 'ant-design-vue';
  import {defineComponent, ref, nextTick} from 'vue';
  //   import {data as options} from '../common/3ktest.js';
  //   import {data as options} from '../common/5ktest.js';
  import {data as options} from '../common/1wtest.js';
​
  export default defineComponent({components: { Tree, Input, Button},
    setup() {
      const replaceFields = {
        title: 'label',
        key: 'id',
      };
      const expandedKeys = ref<string[]>([]);
      const searchValue = ref<string>('');
​
      const onExpand = (keys: string[]) => {expandedKeys.value = keys;};
      const treeData = ref(options);
      
      const search = () => {};
​
      return {
        expandedKeys,
        searchValue,
        treeData,
        replaceFields,
        onExpand,
        search,
      };
    },
  });
</script>

示例代码

and-design 示例

先基于 and-design 来实现一下,间接拿了官网示例代码,数据给做了替换,换成上述一万多条测试数据

import {TreeDataItem} from 'ant-design-vue/es/tree/Tree';
​
const dataList: TreeDataItem[] = [];
const generateList = (data: TreeDataItem[]) => {for (let i = 0; i < data.length; i++) {const node = data[i];
    const id = node.id;
    dataList.push({id, label: node.label});
    if (node.children) {generateList(node.children);
    }
  }
};
generateList(options);
​
const getParentKey = (id: string, tree: TreeDataItem[]): string | number | undefined => {
  let parentKey;
  for (let i = 0; i < tree.length; i++) {const node = tree[i];
    if (node.children) {if (node.children.some((item) => item.id === id)) {parentKey = node.id;} else if (getParentKey(id, node.children)) {parentKey = getParentKey(id, node.children);
      }
    }
  }
  return parentKey;
};
​
const search = () => {
  // 记录含糊查问以及 dom 开展时的耗时工夫
  console.time();
  const value = searchValue.value;
  new Promise((resolve) => {
    const expanded = dataList
      .map((item: TreeDataItem) => {if ((item.label as string).indexOf(value) > -1) {return getParentKey(item.id as string, options);
        }
        return null;
      })
      .filter((item, i, self) => item && self.indexOf(item) === i);
    expandedKeys.value = expanded as string[];
    autoExpandParent.value = true;
    resolve();}).then(async () => {await nextTick();
    // 记录含糊查问以及 dom 开展时的耗时工夫
    console.timeEnd();});
};

页面成果

<img src=”https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/384ddc46b3804a7597534c0489c20a43~tplv-k3u1fbpfcp-watermark.image?” alt=”4.png” width=”70%” />

测试耗时后果

咱们以含糊查问 on 为条件,先看下有多少条数据是合乎 on 为条件的数据

看上述图中有 7831 条数据是合乎含糊查问条件的,那么就是说这些数据以及它们的父节点都要与之相应的开展

那咱们再来看下 示例代码 的耗时

<img src=”https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4e4df573bc8a48788db57009eb566595~tplv-k3u1fbpfcp-watermark.image?” alt=”6.png” width=”100%” />

21227.788818359375 ms这个数字看上去是十分恐怖的,试想哪个用户能在这等个 20 秒呢

解决方案

首先呢在文章首部也写了是哪些起因导致的耗时长,那就来依据这些起因说说解决方案

优化含糊查问所用工夫

因为树形数据是一种嵌套数据结构,通常在查找指标节点时会进行深层次遍历,深层次遍历呢天然耗时会长一点,那咱们晓得,对于遍历来说必定一维的数组还是十分快的,那就能够将树形数据先转化为扁平化数据而后再进行遍历查找指标节点

formatFlatTree办法呢就是接管 树形构造数据 并将其转化为 扁平数据结构 返回,那这个就简略了吧,下列代码不做过多形容了,本人看看

/**
 * @description 格式化树形数据结构为扁平数据结构
 * @param data 传入初始数据
 * @param treeData return 的返回后果
 * @param _params 替换初始数据中 key,title,children 字段为树组件中对应的字段
 * @param _level 层级级别
 * @param parentIds 父节点 id 汇合用来设置 pid
 * @param _params.other 自定义增加须要返回的字段
 */
export function formatFlatTree(
  data,
  _params: any = {},
  _level = 1,
  parentIds: string[] = [],
  treeData: TreeDataState[] = []
) {if (!data.length) {return treeData;}
  const list: TreeDataState[] = [];
  const param = {
    id: _params.id || 'key',
    label: _params.label || 'title',
    children: _params.children || 'children',
    other: _params.other || [],};
  const pIds: string[] = [];
  const obj = {};
  for (let i = 0; i < data.length; i++) {const node = data[i];
    const key = node[param.id];
    const child = node[param.children] || [];
    if (param.other.length) {param.other.forEach((element) => {obj[element] = node[element];
      });
    }
    treeData.push({
      id: key,
      label: node[param.label],
      pid: parentIds[i] || '0',
      level: _level,
      ...obj,
    });
    list.push(...child);
    pIds.push(...new Array(child.length).fill(key));
  }
  return formatFlatTree(list, param, _level + 1, pIds, treeData);
}

优化查问到指标节点后与之相干的节点开展时所耗时间

接着咱们查问到了指标节点然而呢数据是扁平化的构造,但咱们想要的是树形构造,所以呢咱们还须要将扁平数据在合并为树形构造数据

并且咱们须要做的是页面上只展现与指标节点相干的父节点以及祖父节点,无关的给过滤掉不给渲染,这样的话是不是能够节约渲染耗时

获取指标节点与之相干的节点并组成树

export function getFilterTree(source, list) {const initData = JSON.parse(JSON.stringify(list));
  const data = JSON.parse(JSON.stringify(source));
  const obj = {};
  data.forEach((item) => {obj[item.id] = item;
  });
  // 合并残缺树
  data.forEach((item) => {
    let pid = item.pid;
    while (pid) {const parent = obj[pid];
      if (!parent) {const organParent = initData.find((item) => item.id == pid);
        if (organParent) {obj[pid] = organParent;
          pid = organParent.pid;
          data.push(organParent);
        } else {pid = null;}
      } else {pid = null;}
    }
  });
  const trees = flatToTree(data, obj);
  return {data, trees};
}
​
export function flatToTree(data, obj) {const trees: TreeDataState[] = [];
  data.forEach((item) => {const parent = obj[item.pid];
    if (parent) {if (!item.children) {item.children = [];
      }
      (parent.children || (parent.children = [])).push(item);
    } else {trees.push(item);
    }
  });
  return trees;
}

来剖析下上述代码,看是做了什么事件

  1. getFilterTree办法接管两个参数sourcelist

    • source:示意查问到的符合条件的指标节点数据汇合
    • list:示意整个扁平化的数据汇合
  2. 通过 JSON.parse(JSON.stringify() 解决深拷贝问题
  3. 定义一个 obj,将指标数据 id 作为键,指标数据作为值
  4. 再持续通过 forEachwhile对找出符合条件的数据,将与指标节点无关的父节点数据都 push 在 data 中,并将无关的数据以键值对模式都寄存于 obj
  5. flatToTree办法接管两个参数dataobj

    • data:指标节点与之相关联的所有数据
    • obj:以键值对模式寄存指标节点与之相关联的所有数据
  6. 持续通过 forEach 组合残缺树并返回trees
  7. data,与trees 都返回

优化 dom 开展进行分批操作工作

这个时候拿到了查问后的 树形数据 trees,那还须要对 dom 开展 做优化

依据 getFilterTree 办法能够拿到 datadata 呢就是须要开展的 dom 节点的数据

getTreeIds办法返回 data 中的 id 汇合

export function getTreeIds(list) {const ids: TreeDataState[] = [];
  list.map((item) => {ids.push(item.id);
  });
  return ids;
}

下边 示例代码 是将整个汇合都给赋值给 开展指定的树节点属性 ,这种状况呢会导致一次性操作所有的dom 节点 开展,那么必定是会造成页面卡顿的

expandedKeys.value = expanded

来看下我的解决办法

能够将开展 dom 节点 这个工作给它进行拆分,就是说分批给它进行 dom 节点的开展,那这种办法的话效率是十分高的,因为咱们每次只给它开展局部节点,不会操作一次性所有都须要开展的节点

function spread(num: number, index = 0, ids: string[]) {const keys: string[] = [];
  for (let i = 0; i < 50; i++) {if (num <= 0) break;
    num--;
    index++;
    keys.push(ids[index]);
  }
  if (num > 0) {timer = setTimeout(() => {return spread(num, index, ids);
    }, 600);
  }
  expandedKeys.value = [...expandedKeys.value, ...keys];
}
  1. spread办法就是用来做这个工作拆分操作的接管三个参数numindexids

    • num:示意 ids 的长度有多少个节点须要开展
    • index:示意将节点顺次开展
    • ids:示意须要开展的节点汇合
  2. for 循环i < 50,示意每次开展 50 条,可根据集体需要作调整
  3. if(num > 0) 示意还有没开展的节点,须要持续开展
  4. setTimeout 中示意 600ms开展一次,可根据集体需要作调整

优化后代码

const flatTreeData = formatFlatTree(options, { id: 'id', label: 'label'});
// 扁平化数据
console.log(flatTreeData);
​
const search = () => {console.time();
  new Promise((resolve) => {
    const val = searchValue.value;
    
    // 获取指标节点数据
    const flatData = flatTreeData.filter((item) => item.label.includes(val));
    
    // 获取指标节点与之相干的数据,以及组合后新的树形构造数据
    const {data, trees} = getFilterTree(flatData, flatTreeData);
​
    const ids: string[] = getTreeIds(data);
    treeData.value = trees;
    
    // 开展工作分批操作
    spread(ids.length, 0, ids);
    resolve();}).then(async () => {await nextTick();
    console.timeEnd();});
};

优化后成果展示

那咱们再来看看这个时候的耗时是多久

耗时677.60302734375 ms 与 示例代码耗时 做比拟21227.788818359375 ms

看这个数据优化成果还是 十分可观 的。

那这时页面上数据少了好多,那怎么复原原始数据呢,最初清空搜索条件时,能够将 原始树形数据再给渲染下来

以 3000 条数据为基准

不过也很少会用到这么大数据量,那来持续换个 3000 条 测试数据来看下耗时比照

持续以查问 on 为条件,指标节点有 1227 条 数据

示例代码

优化后代码

再来比拟一下 2166.179931640625 ms205.125244140625 ms

那么能够看出后果基于(3000)个节点来说,比 ant-design 均匀的渲染耗时缩小(90%)

当然,大家也能够测试下基于不同个数的节点来说,耗时会缩小多少

结语

扁平化与树形构造数据相互转换的办法能够看下边这篇文章

之前爆燃的高级前端开发不会树形转扁平化格局,你真的会手写了嘛

退出移动版