Высокопроизводительный рендеринг компонентов дерева больших данных

оптимизация производительности

задний план

Проекту необходимо отобразить компонент дерева с более чем 5000 узлов, но при введении элементаКомпонент дереваПозже было обнаружено, что производительность была очень низкой, будь то прокрутка, развертывание/свертывание узлов или нажатие на узлы, это было очень очевидно.Использование производительности для запуска данных о производительности, чтобы найти проблему.

1571390038_96.png
Как видно из приведенного выше рисунка, за исключением Idle, общее затраченное время составляет12s, где Сценарии взяли10s
1571390585_71_w2162_h1088.png
Как видно из приведенного выше рисунка, во время Scripting, кроме Observe, чаще всего вызываетсяcreateChildrenсоздать экземпляр vue

Идеи оптимизации

Из приведенного выше анализа видно, что проблемы с производительностью вызваны тем, что слишком много узлов для рендеринга, поэтому для решения этой проблемы необходимо минимизировать рендеринг узлов.Однако аналогичное решение в отраслиВиртуальный списокОсновная концепция виртуальных списков заключается в том, чтоСписки, управляющие визуализацией видимой области на основе прокруткиТаким образом, рендеринг узлов может быть значительно уменьшен, а производительность может быть повышена.

Конкретные шаги заключаются в следующем:

  1. «Плоские» данные дерева рекурсивной структуры, но сохраняют ссылки родительского и дочернего узлов (с одной стороны, это для удобства нахождения ссылок дочерних и родительских узлов, а с другой стороны, для удобство расчета списка данных видимой области)
  2. динамичныйРассчитать высоту области прокрутки (многие компоненты виртуальных длинных списков имеют фиксированную высоту, но поскольку это дерево, вам нужно свернуть/развернуть узлы, поэтому высота рассчитывается динамически)
  3. Отображает соответствующий узел на основе видимой высоты и расстояния прокрутки.

Код

минимальная реализация кода

<template>
  <div class="b-tree" @scroll="handleScroll">
    <div class="b-tree__phantom" :style="{ height: contentHeight }"></div>
    <div
      class="b-tree__content"
      :style="{ transform: `translateY(${offset}px)` }"
    >
      <div
        v-for="(item, index) in visibleData"
        :key="item.id"
        class="b-tree__list-view"
        :style="{
          paddingLeft: 18 * (item.level - 1) + 'px'
        }"
      >
      <i :class="item.expand ? 'b-tree__expand' : 'b-tree__close' " v-if="item.children && item.children.length" />
        <slot :item="item" :index="index"></slot>
      </div>
    </div>
  </div>
</template>

<style>
.b-tree {
  position: relative;
  height: 500px;
  overflow-y: scroll;
}
.b-tree__phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}
.b-tree__content {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  min-height: 100px;
}
.b-tree__list-view{
  display: flex;
  align-items: center;
  cursor: pointer;
}
.b-tree__content__item {
  padding: 5px;
  box-sizing: border-box;

  display: flex;
  justify-content: space-between;
  position: relative;
  align-items: center;
  cursor: pointer;
}
.b-tree__content__item:hover,
.b-tree__content__item__selected {
  background-color: #d7d7d7;
}
.b-tree__content__item__icon {
  position: absolute;
  left: 0;
  color: #c0c4cc;
  z-index: 10;
}
.b-tree__close{
	display:inline-block;
	width:0;
	height:0;
	overflow:hidden;
	font-size:0;
  margin-right: 5px;
	border-width:5px;
	border-color:transparent transparent transparent #C0C4CC;
	border-style:dashed dashed dashed solid
}
.b-tree__expand{
	display:inline-block;
	width:0;
	height:0;
	overflow:hidden;
	font-size:0;
  margin-right: 5px;
	border-width:5px;
	border-color:#C0C4CC transparent transparent transparent;
	border-style:solid dashed dashed dashed
}
</style>

<script>
export default {
  name: "bigTree",
  props: {
    tree: {
      type: Array,
      required: true,
      default: []
    },
    defaultExpand: {
      type: Boolean,
      required: false,
      default: false
    },
    option: {
      // 配置对象
      type: Object,
      required: true,
      default: {}
    }
  },
  data() {
    return {
      offset: 0, // translateY偏移量
      visibleData: []
    };
  },
  computed: {
    contentHeight() {
      return (
        (this.flattenTree || []).filter(item => item.visible).length *
          this.option.itemHeight +
        "px"
      );
    },
    flattenTree() {
      const flatten = function(
        list,
        childKey = "children",
        level = 1,
        parent = null,
        defaultExpand = true
      ) {
        let arr = [];
        list.forEach(item => {
          item.level = level;
          if (item.expand === undefined) {
            item.expand = defaultExpand;
          }
          if (item.visible === undefined) {
            item.visible = true;
          }
          if (!parent.visible || !parent.expand) {
            item.visible = false;
          }
          item.parent = parent;
          arr.push(item);
          if (item[childKey]) {
            arr.push(
              ...flatten(
                item[childKey],
                childKey,
                level + 1,
                item,
                defaultExpand
              )
            );
          }
        });
        return arr;
      };
      return flatten(this.tree, "children", 1, {
        level: 0,
        visible: true,
        expand: true,
        children: this.tree
      });
    }
  },
  mounted() {
    this.updateVisibleData();
  },
  methods: {
    handleScroll(e) {
      const scrollTop = e.target.scrollTop
      this.updateVisibleData(scrollTop)
    },

    updateVisibleData(scrollTop = 0) {
      const start = Math.floor(scrollTop / this.option.itemHeight);
      const end = start + this.option.visibleCount;
      const allVisibleData = (this.flattenTree || []).filter(
        item => item.visible
      );
      this.visibleData = allVisibleData.slice(start, end);
      this.offset = start * this.option.itemHeight;
    }
  }
};
</script>

Детали следующим образом:

  1. Относительное позиционирование всего контейнера используется, чтобы избежать прокрутки.переформатирование страницы
  2. Фантомный контейнер заставляет полосу прокрутки появляться, чтобы растянуть высоту
  3. Чтобы сгладить древовидные данные рекурсивной структуры, flattenTree добавляет атрибуты level, expand и visibel, которые представляют уровень узла, необходимость развертывания и видимость.
  4. contentHeight динамически вычисляет высоту контейнера, скрытые (свернутые) узлы не должны учитываться в общей высоте

Таким образом, древовидный компонент для рендеринга больших данных имеет базовый прототип.Далее давайте посмотрим, как разворачивать/сворачивать узлы.

Узел Развернуть Свернуть

Ссылка на дочерний элемент сохраняется в flattenTree, чтобы развернуть/свернуть, вам нужно только показать/скрыть дочерний элемент.

{
	methods: {
		 //展开节点
		expand(item) {
		  item.expand = true;
		  this.recursionVisible(item.children, true);
		},
		//折叠节点
		collapse(item) {
		  item.expand = false;
		  this.recursionVisible(item.children, false);
		},
		//递归节点
		recursionVisible(children, status) {
		  children.forEach(node => {
			node.visible = status;
			if (node.children) {
			  this.recursionVisible(node.children, status);
			}
		  })
		}
}

В заключение

Сравните некоторые данные о производительности до и после оптимизации

компонент дерева элементов

Первый рендер (все свернуто)

1575724650_4.png
scripting: 11525ms rendering:2041 мс Примечание: полностью расширенный прямо застрял
1575724867_97.png
scripting: 84ms rendering: 683ms

Оптимизированный компонент дерева

Первый рендер (полное расширение)

1575725392_92.png
scripting:Повышение производительности на 1671 мс по сравнению с до оптимизации6,8 раза rendering:Повышение производительности на 31 мс по сравнению с до оптимизации65 раз

Расширение узла

1575725485_44.png
scripting:86 мс Производительность стабильна до оптимизацииrendering:Повышение производительности на 6 мс по сравнению с до оптимизации113 раз

компоненты большого дерева

окончательная упаковка вvue-big-treeКомпоненты для вызова, добро пожаловать в звезду~~~