vue+element-ui el-table组件二次封装实现虚拟滚动,解决数据量大渲染DOM过多而卡顿问题

2023-11-04

一、此功能已集成到TTable组件中

二、最终效果

在这里插入图片描述

三、需求

某些页面不做分页时,当数据过多,会导致页面卡顿,甚至卡死

四、虚拟滚动

一、固定一个可视区域的大小并且其大小是不变的,那么要做到性能最大化就需要尽量少地渲染 DOM 元素,而这个最小值也就是可视范围内需要展示的内容,而可视区域之外的元素均可以不做渲染。
二、如何计算可视区域内需要渲染的元素,我们通过如下几步来实现虚拟滚动:

1、每一行的高度需要相同,方便计算。
2、需要知道渲染的数据量(数组长度),可基于总量和每个元素的高度计算出容器整体的所需高度,这样就可以伪造一个真实的滚动条。
3、获取可视区域的高度。
4、在滚动事件触发后,滚动条的距顶距离即这个数据量中的偏移量,再根据可视区域本身的高度,算出本次偏移量,这样就得到了需要渲染的具体数据

五、具体实现(源码)

<template>
  <div class="t-table" id="t_table">
    <el-table
      ref="el-table"
      :data="tableData"
      :class="{
        cursor: isCopy,
        row_sort: isRowSort,
        highlightCurrentRow: highlightCurrentRow,
        radioStyle: table.firstColumn && table.firstColumn.type === 'radio',
        treeProps: isShowTreeStyle,
        is_sort_icon:onlyIconSort
      }"
      :max-height="useVirtual?maxHeight||540:maxHeight"
      v-bind="$attrs"
      v-on="$listeners"
      :highlight-current-row="highlightCurrentRow"
      :border="table.border || isTableBorder"
      :span-method="spanMethod || objectSpanMethod"
      :cell-class-name="cellClassNameFuc"
      @sort-change="soltHandle"
      @row-click="rowClick"
      @cell-dblclick="cellDblclick"
    >
      <!-- 主体内容 -->
      <template v-for="(item, index) in renderColumns">
          <el-table-column
            v-if="item.isShowCol === false ? item.isShowCol : true"
            :key="index + 'i'"
            :type="item.type"
            :label="item.label"
            :prop="item.prop"
            :min-width="item['min-width'] || item.minWidth || item.width"
            :sortable="item.sort || sortable"
            :align="item.align || 'center'"
            :fixed="item.fixed"
            :show-overflow-tooltip="useVirtual?true:item.noShowTip?false:true"
            v-bind="{ ...item.bind, ...$attrs }"
            v-on="$listeners"
          >
            <template slot-scope="scope">
              ...
            </template>
          </el-table-column>
      </template>
    </el-table>
   </div>
</template>

<script>
export default {
  name: 'TTable',
  props: {
    // table所需数据
    table: {
      type: Object,
      default: () => {
        return {}
      }
      // required: true
    },
    // 表头数据
    columns: {
      type: Array,
      default: () => {
        return []
      }
      // required: true
    },
    ...
    // Table最大高度
    maxHeight: {
      type: [String, Number]
    },
    // 是否开启虚拟列表
    useVirtual: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      tableData: this.table?.data,
      /**
       * 虚拟列表
       */
      saveDATA: [], // 所有数据
      tableRef: null, // 设置了滚动的那个盒子
      tableWarp: null, // 被设置的transform元素
      fixLeft: null, // 固定左侧--设置的transform元素
      fixRight: null, // 固定右侧--设置的transform元素
      tableFixedLeft: null, // 左侧固定列所在的盒子
      tableFixedRight: null, // 右侧固定列所在的盒子
      scrollTop: 0,
      scrollNum: 0, // scrollTop / (itemHeight * pageList)
      start: 0,
      end: 30, // 3倍的pageList
      starts: 0, // 备份
      ends: 30, // 备份
      pageList: 10, // 一屏显示
      itemHeight: 48 // 每一行高度
    }
  },
  watch: {
    'table.data': {
      handler(val) {
        if (this.useVirtual) {
          this.saveDATA = val
          this.tableData = this.saveDATA.slice(this.start, this.end)
        } else {
          this.tableData = val
        }
      },
      deep: true // 深度监听
    },
    scrollNum(newV) {
      // 因为初始化时已经添加了3屏的数据,所以只有当滚动到第3屏时才计算位移量
      if (newV > 1) {
        this.start = (newV - 1) * this.pageList
        this.end = (newV + 2) * this.pageList
        requestAnimationFrame(() => {
          // 计算偏移量
          this.tableWarp.style.transform = `translateY(${this.start *
            this.itemHeight}px)`
          if (this.fixLeft) {
            this.fixLeft.style.transform = `translateY(${this.start *
              this.itemHeight}px)`
          }
          if (this.fixRight) {
            this.fixRight.style.transform = `translateY(${this.start *
              this.itemHeight}px)`
          }
          this.tableData = this.saveDATA.slice(this.start, this.end)
        })
      } else {
        requestAnimationFrame(() => {
          this.tableData = this.saveDATA.slice(this.starts, this.ends)
          this.tableWarp.style.transform = `translateY(0px)`
          if (this.fixLeft) {
            this.fixLeft.style.transform = `translateY(0px)`
          }
          if (this.fixRight) {
            this.fixRight.style.transform = `translateY(0px)`
          }
        })
      }
    }
  },
  created() {
    // 是否开启虚拟列表
    if (this.useVirtual) {
      this.init()
    }
  },
  mounted() {
    // 是否开启虚拟列表
    if (this.useVirtual) {
      this.initMounted()
    }
  },
  methods: {
    initMounted() {
      this.$nextTick(() => {
        // 设置了滚动的盒子
        this.tableRef = this.$refs['el-table'].bodyWrapper
        // 左侧固定列所在的盒子
        this.tableFixedLeft = document.querySelector(
          '.el-table .el-table__fixed .el-table__fixed-body-wrapper'
        )
        // 右侧固定列所在的盒子
        this.tableFixedRight = document.querySelector(
          '.el-table .el-table__fixed-right .el-table__fixed-body-wrapper'
        )
        /**
         * fixed-left | 主体 | fixed-right
         */
        // 创建内容盒子divWarpPar并且高度设置为所有数据所需要的总高度
        let divWarpPar = document.createElement('div')
        // 如果这里还没获取到saveDATA数据就渲染会导致内容盒子高度为0,可以通过监听saveDATA的长度后再设置一次高度
        divWarpPar.style.height = this.saveDATA.length * this.itemHeight + 'px'
        // 新创建的盒子divWarpChild
        let divWarpChild = document.createElement('div')
        divWarpChild.className = 'fix-warp'
        // 把tableRef的第一个子元素移动到新创建的盒子divWarpChild中
        divWarpChild.append(this.tableRef.children[0])
        // 把divWarpChild添加到divWarpPar中,最把divWarpPar添加到tableRef中
        divWarpPar.append(divWarpChild)
        this.tableRef.append(divWarpPar)
        // left改造
        let divLeftPar = document.createElement('div')
        divLeftPar.style.height = this.saveDATA.length * this.itemHeight + 'px'
        let divLeftChild = document.createElement('div')
        divLeftChild.className = 'fix-left'
        this.tableFixedLeft &&
          divLeftChild.append(this.tableFixedLeft.children[0])
        divLeftPar.append(divLeftChild)
        this.tableFixedLeft && this.tableFixedLeft.append(divLeftPar)
        // right改造
        let divRightPar = document.createElement('div')
        divRightPar.style.height = this.saveDATA.length * this.itemHeight + 'px'
        let divRightChild = document.createElement('div')
        divRightChild.className = 'fix-right'
        this.tableFixedRight &&
          divRightChild.append(this.tableFixedRight.children[0])
        divRightPar.append(divRightChild)
        this.tableFixedRight && this.tableFixedRight.append(divRightPar)
        // 被设置的transform元素
        this.tableWarp = document.querySelector(
          '.el-table .el-table__body-wrapper .fix-warp'
        )
        this.fixLeft = document.querySelector(
          '.el-table .el-table__fixed .el-table__fixed-body-wrapper .fix-left'
        )
        this.fixRight = document.querySelector(
          '.el-table .el-table__fixed-right .el-table__fixed-body-wrapper .fix-right'
        )
        this.tableRef.addEventListener('scroll', this.onScroll)
      })
    },
    // 初始化数据
    init() {
      this.saveDATA = this.table?.data
      this.tableData = this.saveDATA.slice(this.start, this.end)
    },
    // 滚动事件
    onScroll() {
      this.scrollTop = this.tableRef.scrollTop
      this.scrollNum = Math.floor(this.scrollTop / (this.itemHeight * this.pageList))
    }
  }
}
</script>

六、源码地址

GitHub源码地址

Gitee源码地址

基于ElementUi或Antd再次封装基础组件文档

vue3+ts基于Element-plus再次封装基础组件文档

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

vue+element-ui el-table组件二次封装实现虚拟滚动,解决数据量大渲染DOM过多而卡顿问题 的相关文章

随机推荐

  • 最大公约数、最小公倍数、辗转相除法的求解和证明

    两个正整数的最大公约数 Greatest Common Divisor GCD 在计算机中通常使用辗转相除法计算 最小公倍数 Least Common Multiple LCM 可以使用GCD来计算 下面首先介绍GCD和LCM 然后介绍辗转
  • node.js解析xml(xmlreader)

    博客搬家 由于各种原因 我现在的博客将首发于blog mojijs com 可以百度搜索 姜哥的墨迹技术博客 或者 点击这里 本文地址 http blog mojijs com post 19 html xml作为一种重要的数据交换格式 我
  • 图书库毕业设计网页增删改查源码

    介绍 使用HTML VUE PHP MYSQL写的一个简单图书库 实现了简单的数据库增删改查 以及数据列表的展示 源码里包含了前端文件 和api文件 还有数据库表文件 搭建好环境 导入数据库 配置好数据库链接即可直接运行 学习资料地址 ht
  • javaswing基本使用

    package exam test1 import javax swing import java awt import java awt event ActionEvent import java awt event ActionList
  • 三态门——概念,作用,原理

    介绍一下三态门的概念 作用 原理 目录 三态门的概念 三态门的作用 实现总线结构 实现双向数据传输 三态门的原理 三态门的概念 三态门是指逻辑门的输出有三种状态 高电平状态 低电平状态 高阻状态 其中 高阻状态相当于隔离状态 因为高阻状态电
  • linux x64 asm 参数传递,x64 ASM 常用汇编指令

    语法习惯 立即数 开头 寄存器 开头 取地址里面的值 偏移量 寄存器 除了 lea 取地址指令 外 lea就是取地址 load effecive address 整形操作通用后缀 后缀 b w l q 1 2 4 8 byte word l
  • spring源码之@Autowired属性注入

    注入现象 当我们在属性上面加上 Autowired的时候 spring就要根据Type来注入实例了 那么到底会找哪个实例的如果有多个怎么办 今天就来实验一下 多接口注入 当注入的属性接口下有多个实现 这个时候运行的话是 public cla
  • npm link实操详细指南

    准备 首先我们需要有两个npm包 一个作为依赖包 一个作为应用包 依赖包 deps 应用包 app 然后仔仔细细的看一下依赖包的包名和输出路径 如main 好几次用法不对都是因为main字段配置的路径有问题 如我的依赖包package js
  • 多线程(Threading)和多进程(Multiprocessing)

    多线程和多进程 线程和进程是什么 进程间通信方式 线程间通信方式 死锁 Python多线程 Threading 什么是多线程 基本方法函数 join Queue 继承使用线程 同步 GIL锁 锁 Python多进程 多核 Multiproc
  • java.lang.ClassCastException的java类型转换异常解决方案

    在项目中 需要使用XStream将xml string转成相应的对象 却报出了java lang ClassCastException com model test cannot be cast to com model test的错误 原
  • 钟汉良日记:什么副业最好?可以持续带来复利效应的技能,写作!

    2022年9月3日 周六 大晴天 看到一些人做抖音 做自媒体发财了 你是不是也挺羡慕的 可是真要你也去拍摄短视频 做直播真人出镜 大多数人又做不到 怎么办 我觉得 大家还是要认识到一点 打铁还需自身硬 你如果没有足够的对应的能力 只能做一个
  • 【从零开始的Java开发】2-10-2 Servlet入门:Servlet开发步骤、请求参数的发送与接收、Get和Post、注解

    文章目录 概述 软件结构发展史 Tomcat与Servlet Servlet 第一个Servlet JavaWeb工程结构 Servlet开发步骤 请求参数的发送与接收 Get和Post请求方法 Servlet生命周期 使用注解简化配置 启
  • 在Unity中双击打不开vs脚本文件

    从官网下载软件后 创建新项目后 容易漏掉这个设置 导致双击脚本打不开 解决方法 点击找到Edit gt Preferences gt External Tool gt External Script Editor 将对应设置改成你所下载的v
  • 11. TypeScript 条件类型

    TypeScript 条件类型 1 条件类型基本使用 可以使用extends关键字和三元表达式 实现条件判断 interface Fish name1 string interface Water name2 string interfac
  • River Jumping【贪心+模拟】

    题目链接 我们可以贪心的从前往后 每次选最接近的且满足条件的这样的贪心 然后从后往前的时候 就是直接用倒着一个个判断是否合法即可 include
  • 2023百度云智大会:科技与创新的交汇点

    这次的百度云智大会 可谓是亮点云集 发布了包含42个大模型 41个数据集 10个精选应用范式的全新升级千帆大模型平台2 0 发布首个大模型生态伙伴计划 而且也预告了文心大模型4 0的发布 大模型服务的成绩单也非常秀 月活企业数已近万家 覆盖
  • 【已解决】Docker启动失败,报错Cannot connect to the Docker daemon at unix:///var/run/docker.sock.

    报错原因 无法与Docker守护进程建立连接 守护进程负责管理Docker容器和镜像 并提供对Docker API的访问 解决措施 输入以下代码 重启docker服务 service docker start 验证成功 1 输入以下代码 则
  • 如何避免爬虫IP被屏蔽

    各位爬友们好 作为一名专业的爬虫代理提供者 我要和大家分享一些避免爬虫IP被屏蔽的实用技巧 你知道吗 当我们爬取数据的时候 很容易被目标网站识别出来并封禁我们的IP地址 导致无法继续爬取数据 这个问题困扰了很多爬虫程序员 但别担心 今天我就
  • jetbrains IDE设置 phpstorm

    PhpStorm插件 进入 File gt Settings gt Plugins gt Browse repositories 搜索你想要的插件 PHP插件 Symfony Plugin 支持 Symfony 2 3 4 Laravel
  • vue+element-ui el-table组件二次封装实现虚拟滚动,解决数据量大渲染DOM过多而卡顿问题

    一 此功能已集成到TTable组件中 二 最终效果 三 需求 某些页面不做分页时 当数据过多 会导致页面卡顿 甚至卡死 四 虚拟滚动 一 固定一个可视区域的大小并且其大小是不变的 那么要做到性能最大化就需要尽量少地渲染 DOM 元素 而这个