vue.js - el-table-virtual-scroll实现虚拟滚动

el-table-virtual-scroll

基于Element-UI的Table 组件开发的虚拟滚动组件,支持动态高度,解决数据量大时滚动卡顿的问题。

依赖:Vue2和ElementUI。

预览:https://xiaocheng555.github.i...

github:https://github.com/xiaocheng5...

el-table-virtual-scroll.vue 组件代码如下:

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
import throttle from 'lodash/throttle'

export default {
  name: 'el-table-virtual-scroll',
  props: {
    data: {
      type: Array,
      required: true
    },
    height: {
      type: Number,
      default: 60
    },
    buffer: {
      type: Number,
      default: 500
    },
    keyProp: {
      type: String,
      default: 'id'
    },
    throttleTime: {
      type: Number,
      default: 100
    }
  },
  data () {
    return {
      sizes: {} // 尺寸映射(依赖响应式)
    }
  },
  computed: {
    // 计算出每个item(的key值)到滚动容器顶部的距离
    offsetMap ({ keyProp, height, sizes, data }) {
      const res = {}
      let total = 0
      for (let i = 0; i < data.length; i++) {
        const key = data[i][keyProp]
        res[key] = total

        const curSize = sizes[key]
        const size = typeof curSize === 'number' ? curSize : height
        total += size
      }
      return res
    }
  },
  methods: {
    // 初始化数据
    initData () {
      // 可视范围内显示数据
      this.renderData = []
      // 页面可视范围顶端、底部
      this.top = undefined
      this.bottom = undefined
      // 截取页面可视范围内显示数据的开始和结尾索引
      this.start = 0
      this.end = undefined

      this.scroller = this.$el.querySelector('.el-table__body-wrapper')

      // 初次执行
      setTimeout(() => {
        this.handleScroll()
      }, 100)

      // 监听事件
      this.onScroll = throttle(this.handleScroll, this.throttleTime)
      this.scroller.addEventListener('scroll', this.handleScroll)
      window.addEventListener('resize', this.onScroll)
    },

    // 更新尺寸(高度)
    updateSizes () {
      const rows = this.$el.querySelectorAll('.el-table__body > tbody > .el-table__row')

      Array.from(rows).forEach((row, index) => {
        const item = this.renderData[index]
        if (!item) return

        const key = item[this.keyProp]
        const offsetHeight = row.offsetHeight

        if (this.sizes[key] !== offsetHeight) {
          this.$set(this.sizes, key, offsetHeight)
        }
      })
    },

    // 处理滚动事件
    handleScroll (shouldUpdate = true) {
      // 更新当前尺寸(高度)
      this.updateSizes()
      // 计算renderData
      this.calcRenderData()
      // 计算位置
      this.calcPosition()
      shouldUpdate && this.updatePosition()
      // 触发事件
      this.$emit('change', this.renderData, this.start, this.end)
    },

    // 获取某条数据offsetTop
    getOffsetTop (index) {
      const item = this.data[index]
      if (item) {
        return this.offsetMap[item[this.keyProp]] || 0
      }
      return 0
    },

    // 获取某条数据的尺寸
    getSize (index) {
      const item = this.data[index]
      if (item) {
        const key = item[this.keyProp]
        return this.sizes[key] || this.height
      }
      return this.height
    },

    // 计算只在视图上渲染的数据
    calcRenderData () {
      const { scroller, data, buffer } = this
      // 计算可视范围顶部、底部
      const top = scroller.scrollTop - buffer
      const bottom = scroller.scrollTop + scroller.offsetHeight + buffer

      // 二分法计算可视范围内的开始的第一个内容
      let l = 0
      let r = data.length - 1
      let mid = 0
      while (l <= r) {
        mid = Math.floor((l + r) / 2)
        const midVal = this.getOffsetTop(mid)
        if (midVal < top) {
          const midNextVal = this.getOffsetTop(mid + 1)
          if (midNextVal > top) break
          l = mid + 1
        } else {
          r = mid - 1
        }
      }

      // 计算渲染内容的开始、结束索引
      let start = mid
      let end = data.length - 1
      for (let i = start + 1; i < data.length; i++) {
        const offsetTop = this.getOffsetTop(i)
        if (offsetTop >= bottom) {
          end = i
          break
        }
      }

      // 开始索引始终保持偶数,如果为奇数,则加1使其保持偶数【确保表格行的偶数数一致,不会导致斑马纹乱序显示】
      if (start % 2) {
        start = start - 1
      }
      // console.log(start, end, 'start end')

      this.top = top
      this.bottom = bottom
      this.start = start
      this.end = end
      this.renderData = data.slice(start, end + 1)
    },

    // 计算位置
    calcPosition () {
      const last = this.data.length - 1
      // 计算内容总高度
      const wrapHeight = this.getOffsetTop(last) + this.getSize(last)
      // 计算当前滚动位置需要撑起的高度
      const offsetTop = this.getOffsetTop(this.start)

      // 设置dom位置
      const classNames = ['.el-table__body-wrapper', '.el-table__fixed-right .el-table__fixed-body-wrapper', '.el-table__fixed .el-table__fixed-body-wrapper']
      classNames.forEach(className => {
        const el = this.$el.querySelector(className)
        if (!el) return

        // 创建wrapEl、innerEl
        if (!el.wrapEl) {
          const wrapEl = document.createElement('div')
          const innerEl = document.createElement('div')
          wrapEl.appendChild(innerEl)
          innerEl.appendChild(el.children[0])
          el.insertBefore(wrapEl, el.firstChild)
          el.wrapEl = wrapEl
          el.innerEl = innerEl
        }

        if (el.wrapEl) {
          // 设置高度
          el.wrapEl.style.height = wrapHeight + 'px'
          // 设置transform撑起高度
          el.innerEl.style.transform = `translateY(${offsetTop}px)`
          // 设置paddingTop撑起高度
          // el.innerEl.style.paddingTop = `${offsetTop}px`
        }
      })
    },

    // 空闲时更新位置
    updatePosition () {
      this.timer && clearTimeout(this.timer)
      this.timer = setTimeout(() => {
        this.timer && clearTimeout(this.timer)
        // 传入false,避免一直循环调用
        this.handleScroll(false)
      }, this.throttleTime + 10)
    },

    // 【外部调用】更新
    update () {
      console.log('update')
      this.handleScroll()
    },

    // 【外部调用】滚动到第几行
    scrollTo (index, stop = false) {
      const item = this.data[index]
      if (item && this.scroller) {
        this.updateSizes()
        this.calcRenderData()

        this.$nextTick(() => {
          const offsetTop = this.getOffsetTop(index)
          this.scroller.scrollTop = offsetTop

          // 调用两次scrollTo,第一次滚动时,如果表格行初次渲染高度发生变化时,会导致滚动位置有偏差,此时需要第二次执行滚动,确保滚动位置无误
          if (!stop) {
            setTimeout(() => {
              this.scrollTo(index, true)
            }, 50)
          }
        })
      }
    },

    // 【外部调用】重置
    reset () {
      this.sizes = {}
      this.scrollTo(0, false)
    }
  },
  watch: {
    data () {
      this.update()
    }
  },
  created () {
    this.$nextTick(() => {
      this.initData()
    })
  },
  beforeDestroy () {
    if (this.scroller) {
      this.scroller.removeEventListener('scroll', this.onScroll)
      window.removeEventListener('resize', this.onScroll)
    }
  }
}
</script>

<style lang='less' scoped>
</style>

用法

<VirtualScroll
  :data="list"
  :height="62"
  key-prop="id"
  @change="(renderData) => virtualList = renderData">
  <el-table 
    row-key="id" 
    :data="virtualList" 
    height="500px">
  </el-table>
</VirtualScroll>

...

import VirtualScroll from './el-table-virtual-scroll'

export default {
  component: {
    VirtualScroll
  },
  data () {
    list: [
      {
        id: 1,
        text: 'content'
      },
      // ...... 省略n条
      {
        id: 2000,
        text: 'content2'
      }
    ],
    virtualList: []
  }
}

API

Props

参数说明类型可选值默认值
data总数据Array必填
height每一行的预估高度number60
buffer顶部和底部缓冲区域,值越大显示表格的行数越多Number500
keyPropkey值,data数据中的唯一idstringid
throttleTime滚动事件的节流时间number100

Methods

方法名说明参数
scrollTo滚动到第几行index
update更新-
reset重置-

Events

事件名称说明回调参数
change计算完成真实显示的表格行数(renderData, start, end):renderData 真实渲染的数据,start和end指的是渲染的数据在总数据的开始到结束的区间范围

转载于:vue.js - el-table实现虚拟滚动 - 个人文章 - SegmentFault 思否 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值