关于前端:我将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%)

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

结语

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

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

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理