手写一个虚拟表格--仿vxe-table

1,240 阅读2分钟

最近项目中用到了[vxe-table](vxe-table v4 (vxetable.cn))的虚拟表格组件,出于好奇,就仿照手写了一个。效果呈现:

动画 (1).gif

实现原理

虚拟列表主要作用是只渲染可视范围内的DOM元素,通过滚动条的滚动去替换数据,达到数据滚动切换的效果。比如表格需要展示1000条数据,每行高度为50px(只考虑高度一致的情况),可视区域高度500px,那实际渲染的表格行节点只有十个。所以使用虚拟列表可以有效减少渲染的DOM节点。

实现方式

主要结构由外层占位元素、撑开滚动条的元素、实际内容展示元素三部分构成,需要有一个div去撑开滚动条的高度,这个元素的高度就是行高乘以数据总数,实际内容相对于占位元素定位,top等于滚动的高度,让内容始终在可视区域中。

<div class="table" style="position: relative;">
        <div class="table-content">
            <div class="y-space"></div>
            <table :style="{top:滚动高度}" style="position: absolute;left:0"></table>
        </div>
</div>

实现代码

vxe-table的表格组件由表头和内容两部分组成,我也参照了这个实现方式。

表头部分

columns为列配置数组,autoColWidth是列的默认宽度,配置项中没有设置宽度,会使用默认宽度。

<div class="vxe-table-header-wrapper">
  <div class="vxe-table-header">
    <table cellspacing="0" cellpadding="0" border="0">
      <colgroup>
        <col :style="{ width: col.width?`${col.width}px`:`${autoColWidth}px` }" v-for="col in columns" />
      </colgroup>
      <thead>
        <tr class="vxe-header-row">
          <th class="vxe-header-col" v-for="(col,index) in columns">
            <div class="vxe-cell">{{ col.title }}</div>
          </th>
        </tr>
      </thead>
    </table>
  </div>
</div>

const columns = ref([
  { field: 'id',title:'id', width: 200},
  { field: "name", title: "name", width: 200 },
  { field: "sex", title: "sex" },
  { field: "role", title: "role" },
])

const autoColWidth = ref(0)
const scrollBarWidth = ref(20) //滚动条宽度
const getAutoColWidth = () => {
  let count = 0
  const width = columns.value.reduce((width,item) => {
    if(item.width){
      width = width + item.width
    }else{
      count++
    }
    return width
  },0)
  return (vxeTableRef.value?.clientWidth as number - width - scrollBarWidth.value)/count
}

内容部分

<div class="vxe-table-body-wrapper" style="position: relative;" @scroll="handleScroll" :style="{height:`calc(400px - ${rowHeight}px)`}">
        <div class="vxe-table-body">
          <div class="vxe-body--y-space" :style="{height:tableData.length*rowHeight+'px'}"></div>
          <table cellspacing="0" cellpadding="0" border="0" :style="{top:scrollHeight+'px'}" style="position: absolute;left:0">
            <colgroup>
              <col
              :style="{ width: col.width?`${col.width}px`:`${autoColWidth}px` }"
                v-for="col in columns"
              />
            </colgroup>
            <tbody>
              <tr class="vxe-body-row"  v-for="data in filterData" :key="data.id" :name="data.id">
                <th class="vxe-body-col" v-for="(col,index) in columns">
                  <div class="vxe-cell">{{ data[col.field] }}</div>
                </th>
              </tr>
            </tbody>
          </table>
        </div>
</div>

interface TableType {
    id: number;
    name: string;
    sex: string;
    role: string;
}

let tableData:TableType[] = []
const filterData = ref<TableType[]>([])
const count = 10 //展示的行数
const rowHeight = 50 //行高
setTimeout(() => {
  // 模拟数据
  const mockList = []
  for (let index = 0; index < 500; index++) {
    mockList.push({
      id: index,
      name: 'Test' + index,
      role: 'Developer',
      sex: '男'
    })
  }
  tableData = mockList
  filterData.value = tableData.slice(0,count)
}, 100)

let scrollHeight = 0
let scrollStart = 0
let scrollEnd = count
const handleScroll = (e:any) => {
    scrollHeight = e.target?.scrollTop
    scrollStart = Math.floor(e.target?.scrollTop/rowHeight)
    scrollEnd = scrollStart + count
    filterData.value = tableData.slice(scrollStart,scrollEnd)
}

给内容div绑定滚动事件,滚动时去计算该滚动距离时表格数据的下标,截取数据,重新渲染表格。

结语

[源码地址](src/components/vxeTable/index.vue · wuyin/vue3+ts+pinia后台模板 - 码云 - 开源中国 (gitee.com)),用vue3写的,就懒得从项目里抽离了。 项目里还实现了列宽的移动设置,有兴趣的可以自行查看源码。