携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情
前言
上一章我们讲了,前端大数据渲染与优化思路,介绍了解决大数据加载可以使用虚拟列表与时间切片2种方法。也讲了复杂场景适用于虚拟列表的原因,由于element-ui(vue2)的el-table,暂不支持虚拟列表,本文讲针对这个进行改造。
思路
我们在el-table组件上使用指令,然后监听我们的table的滚动事件,当滚动条变化时,计算距离顶部的高度,动态的调整显示的table长度,并且通过transform更新table的位置
el-table结构
- table最外层
<div class="el-table">
</div>
- 加上table的头部header
<div class="el-table">
<div class="el-table__header-wrapper"></div>
</div>
- 加上table的表格部分(table也包含在这里面)
<div class="el-table">
<div class="el-table__header-wrapper"></div>
<div class="el-table__body-wrapper">
<table></table>
</div>
</div>
指令部分
- 通过结构可知,我们需要把el-table的高度固定,然后在table内部新增一个
div节点,此节点的高度为真实数据长度 * 行高(dataLength * rowHeight),那这样当我们在table上滚动滚动条的时候,实际是对此新增的div进行操作。
// 创建dom并添加
const scrollBackgroundElem = document.createElement('div')
scrollBackgroundElem.style.top = `${topHeight}px`
scrollBackgroundElem.style.height = dataLength * rowHeight + 'px'
scrollBackgroundElem.className = scrollBackgroundClass
el.appendChild(scrollBackgroundElem)
- 当没有默认高度或者百分比,我们需要给个默认高度
const tableWrapperElem = el.querySelector(elTableScrollWrapperClass)
const tableHeaderElem = el.querySelector(elTableScrollHeaderClass)
const elHeight = el.style.height
if (!elHeight || elHeight.slice(-2) !== 'px') {
el.style.height = '400px'
}
- 监听滚动事件的方法
const scrollHandler = (e) => {
const scrollTop = e.target.scrollTop
let start = Math.floor(scrollTop / rowHeight)
if (start < 0) start = 0
let end = start + globalRowLimit
// 偏移量
let startOffset = scrollTop - (scrollTop % rowHeight)
tableWrapperElem.style.transform = `translate3d(0,${startOffset}px,0)`
scrollFn(start, end)
页面使用部分
- 我们需要给指令传一个对象,格式可以参考vue官方文档,
v-el-table-dynamic-scroll为我们的组件名,virtualTable是我们定义的一些参数,可以设置一些默认值
const virtualTable = {
// 虚拟table默认值
isOpen: false,
rowLimit: 20,
rowHeight: 40,
throttleTime: 50,
headerHeight: null,
...source,
}
// 指令使用
v-el-table-dynamic-scroll={{
...virtualTable,
rowHeight: 40,
dataLength: this.dataLength,
scrollFn: this.infiniteLoad,
}}
- 这里有个关键的地方,当我们数据发生变化时,我们需要在组件内调用scrollFn,去执行我们页面绑定的
infiniteLoad,这个方法去修改开始和结束的位置
infiniteLoad(start = 0, end = 0) {
this.virtualTableStart = start
this.virtualTableEnd = end
},
- 页面通过计算属性去截取实际展示的元素
virtualData() {
return this.data
? this.data.slice(
this.virtualTableStart,
Math.min(this.virtualTableEnd, this.dataLength),
)
: []
},
组件使用时可以动态添加class去开启虚拟列表
.virtual-table {
overflow: auto;
&:before {
position: sticky;
}
&--empty ::v-deep .el-table__body-wrapper {
top: 0 !important;
width: auto !important;
}
::v-deep {
.el-table__header-wrapper {
position: sticky;
top: 0;
left: 0;
right: 0;
z-index: 1;
}
.el-table__body-wrapper {
transform: translate3d(0, 0, 0);
left: 0;
right: 0;
top: 0;
// position: absolute;
position: initial;
overflow: hidden;
width: fit-content;
height: auto !important;
}
.scroll-background {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
}
}
涉及到部分细节,及初始化的部分就不一一讲解,下面是指令完整代码
import { throttle } from 'throttle-debounce'
const msgTitle = '[el-table-dynamic-scroll]: '
const elTableScrollWrapperClass = '.el-table__body-wrapper'
const elTableScrollHeaderClass = '.el-table__header-wrapper'
const scrollBackgroundClass = 'scroll-background'
let globalDataLen = 0
let globalRowLimit = 0
const ElTableDynamicScroll = {
// 被绑定元素插入父节点时调用
inserted(el, binding) {
const { isOpen, rowHeight, dataLength, scrollFn, rowLimit, headerHeight } =
binding.value
if (!isOpen) return
globalDataLen = dataLength
const tableWrapperElem = el.querySelector(elTableScrollWrapperClass)
const tableHeaderElem = el.querySelector(elTableScrollHeaderClass)
const elHeight = el.style.height
if (!elHeight || elHeight.slice(-2) !== 'px') {
el.style.height = '400px'
}
// 如果当前要显示的数量小于可视区域表格高度,需不断增加显示数量
const showHeight = +el.style.height.slice(0, -2)
globalRowLimit = rowLimit
// 初始化dataLength有值,则会自动添加limit,没值则不会执行,还是使用 rowLimit
while (dataLength && rowHeight * globalRowLimit <= showHeight) {
globalRowLimit++
}
// 初始化赋值
scrollFn(0, globalRowLimit)
if (!tableWrapperElem) {
throw new Error(
`${msgTitle}${elTableScrollWrapperClass} element not found.`,
)
}
// 新增scroll 节点, 用于滚动
setTimeout(() => {
// 动态让父元素宽度等于子元素宽度
const tableWidth =
tableWrapperElem.querySelector('table').style.width || 'fit-content'
tableWrapperElem.style.width = tableWidth
// 解决给组件传高被动态减掉问题
tableHeaderElem.style.width = tableWidth
// header的高度
const topHeight =
headerHeight || tableHeaderElem.getBoundingClientRect().height || 42
// 创建dom并添加
const scrollBackgroundElem = document.createElement('div')
scrollBackgroundElem.style.top = `${topHeight}px`
scrollBackgroundElem.style.height = dataLength * rowHeight + 'px'
scrollBackgroundElem.className = scrollBackgroundClass
el.appendChild(scrollBackgroundElem)
tableWrapperElem.style.top = `${topHeight}px`
listenerElem(el, tableWrapperElem, binding.value)
})
},
componentUpdated(el, binding) {
const { isOpen, rowHeight, dataLength } = binding.value
if (!isOpen) return
// 如果高度相等,无需处理
if (dataLength === globalDataLen) return
// 全局高度更新
globalDataLen = dataLength
const scrollBackgroundElem = el.querySelector(`.${scrollBackgroundClass}`)
// 数据变更后,改变滚动背景的高度,这个值变化,我们的监听滚动条也会变化
if (scrollBackgroundElem) {
scrollBackgroundElem.style.height = dataLength * rowHeight + 'px'
}
},
// 只调用一次,指令与元素解绑时调用。
unbind(el, binding) {
const { isOpen } = binding.value
if (!isOpen) return
// 初始化全局长度
globalDataLen = 0
listenerElem(
el,
el.querySelector(elTableScrollWrapperClass),
binding.value,
false,
)
},
}
export default ElTableDynamicScroll
// 监听与取消监听
function listenerElem(bodyElem, tableWrapperElem, params, isBinding = true) {
const { rowHeight, scrollFn, throttleTime = 50 } = params
if (!bodyElem || !tableWrapperElem) return
const scrollHandler = (e) => {
const scrollTop = e.target.scrollTop
let start = Math.floor(scrollTop / rowHeight)
if (start < 0) start = 0
let end = start + globalRowLimit
// 偏移量
let startOffset = scrollTop - (scrollTop % rowHeight)
tableWrapperElem.style.transform = `translate3d(0,${startOffset}px,0)`
scrollFn(start, end)
}
const throttleFunc = throttle(throttleTime, scrollHandler)
if (isBinding) {
bodyElem.addEventListener('scroll', throttleFunc)
} else {
bodyElem.removeEventListener('scroll', throttleFunc)
}
}
结语
- 本文还使用到了节流函数进行处理,这样可以限制滚动时更新节点的频率。
- 还需要对
scrollTop/transform:translate3d(0,0,0)有一定的了解,不熟悉的小伙伴可以先去查一下用法 - 目前还是存在一些小bug,包括复杂table中的表单校验这种
- 建议基于这个el-table组件进行二次封装,传参数进行配置动态开启虚拟列表,这样避免每次都重新写一套配置。