vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > element 表格el-table虚拟滚动

element表格el-table实现虚拟滚动解决卡顿问题

作者:D浩DzD

当页面数据过多,前端渲染大量的DOM时,会造成页面卡死问题,本文主要介绍了element表格el-table实现虚拟滚动解决卡顿问题,具有一定的参考价值,感兴趣的可以了解一下

当页面数据过多,前端渲染大量的DOM时,会造成页面卡死问题,使用分页或则懒加载这些方案也无法解决,这些处理方法在页面加载到足够多的数据的时候,随着页面追加渲染的DOM越来越多,也会导致页面卡顿,甚至卡死。这时候我们可以把两个方案中和一下,既然在有限的视窗中我们只能看到一部分的数据,那么我们就通过计算可视范围内的单元格,这样就保证了每一次拖动,我们渲染的 DOM 元素始终是可控的,不会像数据分页方案怕一次性渲染过多,也不会发生无限滚动方案中的老数据堆积现象。所以就有了虚拟滚动这一方案。

虚拟滚动

接下来我们用一张图来表示虚拟滚动的表现形式。

根据图中我们可以看到,无论我们如何滚动,我们可视区域的大小其实是不变的,那么要做到性能最大化就需要尽量少地渲染 DOM 元素,而这个最小值也就是可视范围内需要展示的内容,也就是图中的绿色区块,在可视区域之外的元素均可以不做渲染。

那么问题就来了,如何计算可视区域内需要渲染的元素,我们通过如下几步来实现虚拟滚动:

效果如图:

<template>
  <el-table
    :data="tableData"
    ref="tableRef"
    style="width: 900px"
    max-height="380"
    border
    stripe
    class="myTable"
  >
    <el-table-column
      prop="date"
      label="必要元素:"
      min-width="150"
      align="center"
      fixed="left"
    >
    </el-table-column>
    <el-table-column label="每一行高度必须相同">
      <el-table-column
        prop="name"
        label="class不能为【myTable】"
        min-width="180"
        align="center"
      >
      </el-table-column>
      <el-table-column label="ref不能为【tableRef】">
        <el-table-column
          prop="province"
          label="省份"
          min-width="150"
          align="center"
        >
        </el-table-column>
        <el-table-column
          prop="city"
          label="市区"
          min-width="150"
          align="center"
        >
        </el-table-column>
        <el-table-column
          prop="address"
          label="地址"
          min-width="150"
          align="center"
        >
        </el-table-column>
      </el-table-column>
    </el-table-column>
    <el-table-column label="操作" fixed="right" min-width="160" align="center">
      <template>
        <el-button size="mini">编辑</el-button>
        <el-button size="mini" type="danger">删除</el-button>
      </template>
    </el-table-column>
  </el-table>
</template>
<script>
export default {
  data() {
    return {
      tableData: [], // 需要渲染的数据
      saveDATA: [], // 所有数据
      tableRef: null, // 设置了滚动的那个盒子
      tableWarp: null,
      fixLeft: null,
      fixRight: null,
      tableFixedLeft: null,
      tableFixedRight: null,
      scrollTop: 0,
      num: 0,
      start: 0,
      end: 42, // 3倍的pageList
      starts: 0, // 备份[保持与上一样]
      ends: 42, // 备份[保持与上一样]
      pageList: 14, // 一屏显示
      itemHeight: 41, // 每一行高度
      timeOut: 400 // 延迟
    }
  },
  watch: {
    num: function(newV) {
    // 因为初始化时已经添加了3屏的数据,所以只有当滚动到第3屏时才计算位移量
      if (newV > 1) {
        this.start = (newV - 1) * this.pageList
        this.end = (newV + 2) * this.pageList
        // 使用延时器会造成页面短暂白屏的问题,使用requestAnimationFrame则可以解决
        // setTimeout(() => {
        // // 计算偏移量
        //   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)
        // }, this.timeOut)
        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)`
          }
        })
        // setTimeout(() => {
        //   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)`
        //   }
        // }, this.timeOut)
      }
    }
  },
  created() {
    this.init()
  },
  mounted() {
    this.$nextTick(() => {
    // 设置了滚动的盒子
      this.tableRef = this.$refs.tableRef.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)
    })
  },
  methods: {
    init() {
      this.saveDATA = []
      for (let i = 0; i < 10000; i++) {
        this.saveDATA.push({
          date: i,
          name: '王小虎' + i,
          address: '1518',
          province: 'github:',
          city: 'divcssjs',
          zip: 'divcssjs' + i
        })
      }
      this.tableData = this.saveDATA.slice(this.start, this.end)
    },
    onScroll() {
      this.scrollTop = this.tableRef.scrollTop
      this.num = Math.floor(this.scrollTop / (this.itemHeight * this.pageList))
    }
  }
}
</script>
<style lang="less" scoped>
.myTable {
  /deep/ td {
    padding: 6px 0 !important;
  }
}
/*滚动条样式*/
/deep/ .el-table__body-wrapper::-webkit-scrollbar {
  /*滚动条整体样式*/
  width: 6px;
  /*高宽分别对应横竖滚动条的尺寸*/
  height: 8px;
}
/deep/ .el-table__body-wrapper::-webkit-scrollbar-thumb {
  /*滚动条里面小方块*/
  border-radius: 2px;
  background: #666;
}
/deep/ .el-table__body-wrapper::-webkit-scrollbar-track {
  /*滚动条里面轨道*/
  background: #ccc;
}
</style>

使用虚拟列表做投屏功能

投屏页面 buletinFullscreen.vue

<!--
 * @Last Modified by:   Damon
 * @Last Modified time: 2023-2-14
 * @content: 订单看板投屏
-->
<template>
	<!-- 设置虚拟列表滚动条的盒子 -->
  <div id="buletinFullscreen">
    <div class="list-top">
      <p class="list-tit">订单看板</p>
      <ul class="list-lists">
        <li>已接单(数量): <span style="color: #008300">1</span></li>
        <li>生产中(数量): <span style="color: #D3831A">2</span></li>
        <li>出货中(数量): <span style="color: #128BFF">3</span></li>
        <li>出货完成(数量): <span style="color: #585858">4</span></li>
      </ul>
      <button
        class="cancel-btn"
        size="mini"
        @click="exitListFullScreen"
      >{{ $t('qxtp') }}</button>
    </div>
	<!-- 设置虚拟列表这个盒子的总高度,即所有数据显示所需要的高度,比如100*163,其中100为所有数据条数,163为每条列表展示的高度 -->
    <div class="list-div-content" id="buletinScrollBox" ref="contentBox">
			<!-- 真正展示内容的盒子,也是设置transform的盒子 -->
          <div id="buletinScrollContent" v-if="tableDataFullscreen && tableDataFullscreen.length>0">
		  <!-- 每一项 -->
            <div class="list-title-div" v-for="(item) in tableDataFullscreen" :key="item.id">
              {{ item.code }}
            </div>
          </div>
        </div>
  </div>
</template>
<script>
export default {
  components: {},
  props: {},
  data () {
    return {
      tableDataFullscreen: [], // 需要显示的数据
      scrollDomBox: null, // 设置了滚动的那个盒子
      noScroll: true, // 是否没有滚动条
      isBottom: false,
      setIntervalTimer: null,
      setTimeoutTimer: null,
      setTimeoutTimer1: null,
      allTableDataFullscreen: [], // 所有数据
      tableWarp: null,
      scrollTop: 0,
      num: 0,
      start: 0,
      end: 90, // 3倍的pageList
      starts: 0, // 备份[保持与上一样]
      ends: 90, // 备份[保持与上一样]
      pageList: 30, // 需要显示的数据的条数
	  clientList: 10, // 可视区域能显示的条数
      itemHeight: 110, // 每一行高度
    }
  },
  watch: {
    isBottom: function() {
      // 触底和触顶后重新请求一次数据
      setTimeout(() => {
        this.clearIntervalFn()
        this.getListData(true)
      },5000)
    },
    num: function(newV) {
    // 因为初始化时已经添加了3屏的数据,所以只有当滚动到第3屏时才计算位移量
	console.log(newV,'newV');
      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)`
          this.tableDataFullscreen = this.allTableDataFullscreen.slice(this.start, this.end)
        })
      } else {
        requestAnimationFrame(() => {
          this.tableDataFullscreen = this.allTableDataFullscreen.slice(this.starts, this.ends)
          this.tableWarp.style.transform = `translateY(0px)`
        })
      }
    }
  },
  created() {},
  mounted() {
    this.getListData()
  },
  destroyed() {
    // 清除定时器和延时器
    this.clearIntervalFn()
    this.clearTimeoutFn(this.setTimeoutTimer)
    this.clearTimeoutFn(this.setTimeoutTimer1)
    // 取消监听全屏事件
    this.removeScreenFn()
  },
  methods: {
	  // 计算当前屏数
    onScroll() {
      this.scrollTop = this.scrollDomBox.scrollTop
      this.num = Math.floor(this.scrollTop / (this.itemHeight * this.pageList))
    },
    clearIntervalFn() {
      clearInterval(this.setIntervalTimer)
    },
    clearTimeoutFn(timer) {
      clearTimeout(timer)
    },
    getListData(isFalg) {
		if (!isFalg) {
			this.listenerScreenFn()
		}
		let arr = []
		// 模拟异步请求
		setTimeout(() => {
			for (var i = 0; i < 10000; i++) {
				let obj = {
					code: '列表' + i
				}
				arr.push(obj)
			}
			this.allTableDataFullscreen = arr;
			// 首屏的时候要多加上一屏的数据才能无缝衔接
			const count = this.pageList + this.clientList;
			this.tableDataFullscreen = this.allTableDataFullscreen.slice(0,count)
			this.$nextTick(() => {
				// 设置了滚动的盒子
				this.scrollDomBox = document.getElementById("buletinFullscreen");
				// 高度设置为所有数据所需要的总高度
				let divWarpPar = document.getElementById('buletinScrollBox')
				// 如果这里还没获取到数据就渲染会导致内容盒子高度为0,可以通过监听长度后再设置一次高度
				// 头部高度为80
				if (divWarpPar) {
					divWarpPar.style.height = this.allTableDataFullscreen.length * this.itemHeight + 80 + 'px'
				}
				console.log(this.allTableDataFullscreen.length * this.itemHeight,'kkk');
				// 被设置的transform元素
				this.tableWarp = document.getElementById('buletinScrollContent')
				this.scrollDomBox && this.scrollDomBox.addEventListener('scroll', this.onScroll)
				let scrollBox = document.getElementById("buletinFullscreen");
				// 电脑屏幕的刷新率为60左右
				this.setIntervalTimer = setInterval(() => {
				  this.autoScroll(scrollBox)
				  this.onScroll()
				},60)
			})
		},2000)
    },
    // 自动滚动
    autoScroll(scrollDom) {
      if ((scrollDom.scrollTop + scrollDom.clientHeight < scrollDom.scrollHeight && !this.isBottom) || !scrollDom.scrollTop) {
        scrollDom.scrollTop += 1
        this.isBottom = false
      } else {
		  // 触底则向上滚动
        scrollDom.scrollTop -= 1
        this.isBottom = true
      }
      if (!scrollDom.scrollTop && this.noScroll) {
        this.noScroll = false
        // 没有滚动条时触发
        this.setTimeoutTimer1 = setTimeout(() => {
          this.clearIntervalFn()
          this.getListData()
        },30000)
      }
    },
    /**
     * @description 全屏
	 * requestFullscreen方法必须由用户主动交互触发,否则会报错
     */
    listFullScreen() {
      const element = document.documentElement
      if (element.requestFullscreen) {
        element.requestFullscreen()
      } else if (element.mozRequestFullScreen) {
        element.mozRequestFullScreen()
      } else if (element.msRequestFullscreen) {
        element.msRequestFullscreen()
      } else if (element.webkitRequestFullscreen) {
        element.webkitRequestFullscreen()
      }
    },
    /**
     * @description 退出全屏
     */
    exitListFullScreen() {
      if (document.fullscreenElement) {
        document.exitFullscreen()
      } else if (document.mozCancelFullScreen) {
        document.mozCancelFullScreen()
      } else if (document.msExitFullscreen) {
        document.msExitFullscreen()
      } else if (document.webkitExitFullscreen) {
        document.webkitExitFullscreen()
      }
    },
    changeFullscreen() {
      console.log('监听全屏事件');
      const fullscreenEle = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement
      console.log(fullscreenEle,'fullscreenEle');
      let flag = false
        if (fullscreenEle) {
          //浏览器进入全屏
          flag = true
        } else {
          flag = false
        }
        this.$emit('changeShowFlag',flag)
    },
        /**
     * @description 监听全屏事件
     */
    listenerScreenFn() {
      this.listFullScreen()
      if (document.exitFullscreen) {
        document.addEventListener('fullscreenchange', this.changeFullscreen)
      } else if (document.mozCancelFullScreen) {
        document.addEventListener('mozfullscreenchange', this.changeFullscreen)
      } else if (document.msExitFullscreen) {
        document.addEventListener('MSFullscreenChange', this.changeFullscreen)
      } else if (document.webkitCancelFullScreen) {
        document.addEventListener('webkitfullscreenchange', this.changeFullscreen)
      }
    },
    /**
    * @description 移除全屏事件
    */
    removeScreenFn() {
      if (document.exitFullscreen) {
        document.removeEventListener('fullscreenchange', this.changeFullscreen)
      } else if (document.mozCancelFullScreen) {
        document.removeEventListener('mozfullscreenchange', this.changeFullscreen)
      } else if (document.msExitFullscreen) {
        document.removeEventListener('MSFullscreenChange', this.changeFullscreen)
      } else if (document.webkitCancelFullScreen) {
        document.removeEventListener('webkitfullscreenchange', this.changeFullscreen)
      }
    },
  }
}
</script>
<style lang="less" scoped>
#buletinFullscreen {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  overflow-y: auto;
  z-index: 1002;
  .list-top{
    position: fixed;
    z-index: 1003;
    width:100%;
    transition: top .3s ease;
    border-bottom:1px solid #ebeef2 ;
    margin-left: 0px;
    padding: 0 15px;
    height: 80px;
    display: flex;
    align-items: center;
    color: #333333;
    .cancel-btn {
      position: absolute;
      top: 25px;
      right: 15px;
    }
  }
  .list-tit{
    margin-right: 30px;
    margin-left: 10px;
    font-weight: 500;
    font-size: 24px;
  }
  .list-lists{
    display: flex;
    margin-top: 0px;
    li{
      margin-right: 30px;
    }
  }
  .list-div-content{
    padding: 95px 15px 15px;
    background-color: #ebeef2;
    min-height: 100%;
    overflow: hidden;
    .list-title-div{
		height: 100px;
      width: 100%;
      display: flex;
      flex-direction: column;
      justify-content: center;
      padding: 10px 15px;
      background: #ffffff;
      margin-bottom: 10px;
      position: relative;
	  overflow: hidden;
    }
  }
}
</style>

引入投屏页

<template>
	<div>
		<button
		  size="mini"
		  @click="handlerClick"
		>投屏</button>
		<!-- 投屏页面 必须使用v-if -->
		<buletin-fullscreen v-if="isShowFullScreen" @changeShowFlag="handlerClick"></buletin-fullscreen>
	</div>
</template>
<script>
import buletinFullscreen from '~/components/bulletin/buletinFullscreen.vue';
export default {
  components: {
	  buletinFullscreen
  },
  data() {
    return {
      isShowFullScreen:false,// 是否全屏
    };
  },
  mounted() {
    window.addEventListener("keydown", this.keyDownFuns)// 监听按键事件
  },
  methods: {
    // 监听F11键盘事件
    keyDownFuns(event) {
      if (event.keyCode === 122) {
        event.preventDefault();
        this.handlerClick(true)
      }
    },
	handlerClick(flag) {
		this.isShowFullScreen= flag
	}
  }
};
</script>
<style lang="less" scoped>
</style>

element表格组件实现虚拟滚动,解决卡顿问题

「前端进阶」高性能渲染十万条数据(时间分片)

到此这篇关于element表格el-table实现虚拟滚动解决卡顿问题的文章就介绍到这了,更多相关element 表格el-table虚拟滚动内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文