优秀的编程知识分享平台

网站首页 > 技术文章 正文

Vue高性能渲染大数据Tree组件「实践」

nanyue 2024-10-25 13:12:37 技术文章 2 ℃


作者:jayzou

转发连接:https://segmentfault.com/a/1190000021228976

背景

项目中需要渲染一个5000+节点的树组件,但是在引入element Tree组件之后发现性能非常差,无论是滚动、展开/收起节点还是点击节点卡顿都非常明显,利用performance跑一下性能数据发现到问题所在

从上图可以看到,除去Idle之外,总共花费的时间为12s,其中Scripting花了10s

从上图可以看出,Scripting期间除了 Observe 之外,大部分时间都在调用createChildren来创建vue实例

优化思路

从上面的分析可以看出引发的性能问题都是因为渲染的节点过多导致,那么要解决这个问题就是尽量减少节点的渲染,然而在业界中与之相类似的解决方案就是虚拟列表虚拟列表的核心概念就是 根据滚动来控制可视区域渲染的列表这样一来,就能大幅度减少节点的渲染,提升性能

具体的步骤如下:

  1. 将递归结构的tree数据“拍平”,但是保留parent以及child的引用(一方面是为了方便查找子级和父级节点的引用,另一方面是为了方便计算可视区域的list数据)
  2. 动态计算滚动区域的高度(很多虚拟长列表的组件都是固定高度的,但是因为这里是tree,需要折叠/展开节点,所以是动态计算高度)
  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. phantom 容器为了撑开高度,让滚动条出现
  3. flattenTree 为了拍平 递归结构的tree数据,同时添加level、expand、visibel属性,分别代表节点层级、是否展开、是否可视
  4. contentHeight 动态计算容器的高度,隐藏(收起)节点不应该计算在总高度里面

这样一来渲染大数据的tree组件就有了基本的雏形,接下来看看节点展开/收起如何实现

节点展开收起

在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);
            }
          })
        }
}

结论

对比下优化前和优化后的一些性能数据

element tree组件

初次渲染(全收起)

scripting: 11525msrendering: 2041ms注:全展开直接卡死

scripting: 84msrendering: 683ms

优化后的tree组件

首次渲染(全展开)

scripting: 1671ms 对比优化前性能提升 6.8倍rendering: 31ms 对比优化前性能提升 65倍

节点展开

scripting: 86ms 优化前性能一致

rendering: 6ms 对比优化前性能提升 113倍

Github地址 :https://github.com/jayZOU/vue-big-tree

推荐Vue学习资料文章:

尤大大细品VuePress搭建技术网站与个人博客「实践」

10个Vue开发技巧「实践」

是什么导致尤大大选择放弃Webpack?【vite 原理解析】

带你了解 vue-next(Vue 3.0)之 小试牛刀【实践】

带你了解 vue-next(Vue 3.0)之 初入茅庐【实践】

实践Vue 3.0做JSX(TSX)风格的组件开发

一篇文章教你并列比较React.js和Vue.js的语法【实践】

手拉手带你开启Vue3世界的鬼斧神工【实践】

深入浅出通过vue-cli3构建一个SSR应用程序【实践】

怎样为你的 Vue.js 单页应用提速

聊聊昨晚尤雨溪现场针对Vue3.0 Beta版本新特性知识点汇总

【新消息】Vue 3.0 Beta 版本发布,你还学的动么?

Vue真是太好了 壹万多字的Vue知识点 超详细!

Vue + Koa从零打造一个H5页面可视化编辑器——Quark-h5

深入浅出Vue3 跟着尤雨溪学 TypeScript 之 Ref 【实践】

手把手教你深入浅出vue-cli3升级vue-cli4的方法

Vue 3.0 Beta 和React 开发者分别杠上了

手把手教你用vue drag chart 实现一个可以拖动 / 缩放的图表组件

Vue3 尝鲜

总结Vue组件的通信

手把手让你成为更好的Vue.js开发人员的12个技巧和窍门【实践】

Vue 开源项目 TOP45

2020 年,Vue 受欢迎程度是否会超过 React?

尤雨溪:Vue 3.0的设计原则

使用vue实现HTML页面生成图片

实现全栈收银系统(Node+Vue)(上)

实现全栈收银系统(Node+Vue)(下)

vue引入原生高德地图

Vue合理配置WebSocket并实现群聊

多年vue项目实战经验汇总

vue之将echart封装为组件

基于 Vue 的两层吸顶踩坑总结

Vue插件总结【前端开发必备】

Vue 开发必须知道的 36 个技巧【近1W字】

构建大型 Vue.js 项目的10条建议

深入理解vue中的slot与slot-scope

手把手教你Vue解析pdf(base64)转图片【实践】

使用vue+node搭建前端异常监控系统

推荐 8 个漂亮的 vue.js 进度条组件

基于Vue实现拖拽升级(九宫格拖拽)

手摸手,带你用vue撸后台 系列二(登录权限篇)

手摸手,带你用vue撸后台 系列三(实战篇)

前端框架用vue还是react?清晰对比两者差异

Vue组件间通信几种方式,你用哪种?【实践】

浅析 React / Vue 跨端渲染原理与实现

10个Vue开发技巧助力成为更好的工程师

手把手教你Vue之父子组件间通信实践讲解【props、$ref 、$emit】

1W字长文+多图,带你了解vue的双向数据绑定源码实现

深入浅出Vue3 的响应式和以前的区别到底在哪里?【实践】

干货满满!如何优雅简洁地实现时钟翻牌器(支持JS/Vue/React)

基于Vue/VueRouter/Vuex/Axios登录路由和接口级拦截原理与实现

手把手教你D3.js 实现数据可视化极速上手到Vue应用

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【上】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【中】

吃透 Vue 项目开发实践|16个方面深入前端工程化开发技巧【下】

Vue3.0权限管理实现流程【实践】

后台管理系统,前端Vue根据角色动态设置菜单栏和路由

作者:jayzou

转发连接:https://segmentfault.com/a/1190000021228976

Tags:

最近发表
标签列表