本文介绍内容包括:javascript
- Element UI 实现表头表列固定思考与总结
translate3d
如何实现表头表列固定
书承上文,在前文【Vue进阶】青铜选手,如何自研一套UI库中介绍了Vue组件库的开发细节,举例实现了button、table等组件的开发。在Ange这个UI库中,我实现了一个内容高可定制的表格组件:可固定表头和表列,内容则自行定义。html
首先要认可,这个table组件实现的功能很简单:java
表格组件是UI库里面最为复杂的组件之一,项目中使用表格的场景特别多,咱们很难覆盖全部人的需求,比较常见的就有:git
从做用对象来看,这些需求又可归为影响布局(Eg: 固定表头表列)和影响数据(Eg: 勾选数据)两个大类。在Ange UI的table组件中,仅仅实现了影响布局这个类下面的部分功能,该组件不操做数据,甚至具体到使用tr、td标签(以及td里面如何包裹数据)展现数据也是由使用者本身定义的。狠狠点击这里在线查看示例,或者查看代码:github
<ag-table offsetTop="57.5">
<tr slot="thead">
<!-- 定义表头列 -->
<th v-if="isExpand">姓名</th>
<th v-for="(each, index) in singleTableHead" :key="index">{{ each }}</th>
</tr>
<tr v-for="(each, index) in singleTableBody" slot='tbody' :key="`tbody-${index}`">
<!-- 渲染表体内容 -->
<td v-if="isExpand">{{ each.name }}</td>
<td>{{ each.verdict }}</td>
<td>{{ each.song }}</td>
</tr>
</ag-table>
复制代码
经过插槽slot指定thead或是tbody。简单就意味着精细和可拓展性强,同时带来的问题就是用户的使用成本高了(好比实现数据选择功能,固然ag-table
在不操做源数据的原则下也能拓展出这个功能)。浏览器
从浏览器中审查Element table组件的渲染效果看,Element实现固定表头表列的方式是:将固定的部分(如表头)和不固定的部分(如表体)拆分放在不一样区域(不一样的div下),设置表体所在区域可滚动便可,而后再经过必定的手段(如阴槽、表格数据备份)去同步不一样区域之间的布局。app
在一篇饿了么专题的文章中,详细阐述了固定表头表列的实现。下面简单总结并整理其中存在的问题。布局
从浏览器中审查table组件的渲染效果看: post
el-table__header-wrapper
&
el-table__body-wrapper
,如此表体内容超出容器高度时,会出现滚动条,只在本身区域内滚动,达到了表头固定的效果。这样的实现
致使了两个问题:
针对上面的问题,element也作了处理,引用饿了么文中一张图片: 性能
这种实现方式有什么缺点呢?
实现固定表列相对比较复杂,为实现这个功能,element可谓是付出了“巨大的成本”。在这个左右列固定的渲染效果中:
el-table__header-wrapper
&
el-table__body-wrapper
是表体区域,
el-table__fixed
是左固定列区域、
el-table__fixed-right
是右固定列区域),每一份表格又有2个table,
一共是6个table;经过设置左右区域绝对定位和宽度实现固定的效果。
这样实现会有什么问题呢?
基于此,Ange UI的table实现考虑用另一种方式去实现,达到了最低的DOM成本。
在介绍固定表头表列实现方法以前,先科普下getBoundingClientRect这个API。
getBoundingClientRect()方法返回元素的大小及其相对视口的位置,它的返回值是一个DOMRect对象。DOMRect对象包含了一组用于描述边框的只读属性:left、right、top、bottom,单位为像素。除了width和height外的属性都是相对于视口的左上角而言的。
以下图:
在一个table中分别用thead和tbody展现表头表体,以下代码:
<template>
<div class="ange-table">
<table ref="middle-table">
<thead class="thead-middle" :style="theadStyle">
<slot name="thead" />
</thead>
<tbody>
<slot name="tbody" />
</tbody>
</table>
</div>
</template>
复制代码
监听页面滚动事件,计算table的位移,使用translate3d
反向设置thead的y轴位移值,达到固定表头的效果。以下图:
translate3d(0px, -top2, 0px)
。这样,thead就一直处在页面顶端位置了。 在某些场景下,thead达到Header的位置时就应该被fixed了,那们咱们能够设置一个
offsetTop
参数,用户自定义偏移值,thead在
top=0 - offsetTop
时被fixed。看关键实现代码:
export default {
data () {
return: {
fixed: { // fixed状态
top: false
},
clientRect: { // 位移值
top: 0
}
}
},
computed: {
theadStyle () {
const { top } = this.clientRect
return {
transform: `translate3d(0px, ${this.fixed.top ? -top : 0}px, 1px)`
}
}
},
watch: {
'clientRect.top': function (val) {
// 判断到DOMRect的top值小于0时,开始fixed
this.fixed.top = val < 0
}
},
mounted () {
// 监听页面滚动事件,获取table对象的DOMRect属性
window.addEventListener('scroll', this.scrollHandle, {
capture: false,
passive: true
})
},
methods: {
scrollHandle () {
const $table = this.$refs.table
if(!$table) return
const { top } = $table.getBoundingClientRect()
this.clientRect.top = Math.floor(top - parseInt(this.offsetTop, 10))
}
}
}
复制代码
结合 @前言 部分ag-table的使用示例,在<ag-tbale>
中传入一个offsetTop
参数,便可实现thead在指定位置的fixed。另因为thead和tbody在同一个table中,不须要维护每一列的宽度,它能够根据内容自适应。查看demo。
固定列的实现须要三个表格(分别固定左列和右列),以下代码:
<template>
<div class="ange-table">
<!-- left table -->
<table v-if="hasLeftTable" ref="leftTable" :style="leftStyle">
<thead class="thead-left" :style="theadStyle">
<slot name="leftThead" />
</thead>
<tbody>
<slot name="leftBody" />
</tbody>
</table>
<!-- middle table -->
<table ref="table" class="table-middle">
<thead class="thead-middle" :style="theadStyle">
<slot name="thead" />
</thead>
<tbody>
<slot name="tbody" />
</tbody>
</table>
<!-- right table -->
<table v-if="hasRightTable" ref="rightTable" :style="rightStyle">
<thead class="thead-right" :style="theadStyle">
<slot name="rightThead" />
</thead>
<tbody>
<slot name="rightBody" />
</tbody>
</table>
</div>
</template>
复制代码
table横向滚动时,计算容器的横向滚动距离scrollLeft
,使用translate3d
反向设置左table的x轴位移值,固定左列;对于右table,先要将其初始位置设置在容器的最右端,横向滚动时再结合scrollLeft设置x轴的位移值。以下图:
$rightTable.right - $container.right
,
leftTable就是0;发生横向滚动时,
leftTable的横向位移值:
scrollLeft
,
rightTable的位移值:
初始位移 - scrollLeft
。看关键实现代码:
export default {
computed: {
leftStyle () { // 左侧表格位移
const { left } = this.clientRect
return {
transform: `translate3d(${this.fixed.left ? left : 0}px, 0px, 1px)`
}
},
rightStyle () { // 右侧表格位移
const { right } = this.clientRect
return {
transform: `translate3d(${-right}px, 0px, 1px)`
}
}
},
watch: {
'clientRect.left': function (val) {
// 横向滚动距离为正,开始设置fixed
this.fixed.left = val > 0
}
},
mounted () {
// 存在由表格时设置其初始位移
if(this.hasRightTable) {
const container = this.$refs.container.getBoundingClientRect()
const rightTable = this.$refs.rightTable.getBoundingClientRect()
this.clientRect.right = Math.floor(rightTable.right - container.right)
// 记录右表格初始位移值
this.initRight = this.clientRect.right
}
// 监听表格容器的滚动事件
this.$refs.container.addEventListener('scroll', this.scrollXHandle, {
capture: false,
passive: true
})
// ...
},
methods: {
scrollXHandle () {
// ...
this.clientRect.left = Math.floor(this.$refs.container.scrollLeft)
const right = Math.floor(this.initRight - this.$refs.container.scrollLeft)
this.clientRect.right = right
}
}
}
复制代码
按照这个思路实现左右列固定,效果以下(在线查看):
最后一步,由于这个表格是由三份table组成,所以当鼠标hover在其中一个table行上时,须要在剩余两个table的对应行中同步hover效果。看关键代码的实现:
export default {
mounted () {
if(this.hasLeftTable || this.hasRightTable) {
// 定义鼠标hover事件
this.$el.addEventListener('mouseover', this.mouseOver, false)
this.$el.addEventListener('mouseout', this.mouseLeave, false)
}
},
methods: {
mouseOver (e) {
this.hoverClass(e, 'add')
},
mouseLeave(e) {
this.hoverClass(e, 'remove')
},
hoverClass(e, type) {
const tr = e.target.closest('tr')
if(!tr) {
return
}
const idx = tr.rowIndex // 当前hover行的编号
const trs = querySelectorAll(`tbody tr:nth-child(${idx})`, this.$el)
if(trs.length === 0) {
return
}
// 对全部tbody下同一编号的tr添加hover类
trs.forEach(each => {
each.classList[type]('hover')
})
}
}
}
复制代码
经过translate3d
设置左右列的位移实现固定列的效果,避免了:
table组件一直是开发复杂度较高的组件,既要考虑性能,又要考虑尽量地对开发者使用友好。在此抛砖引玉,提供另外一种开发思路,只为给有计划开发table组件的你提供一点帮助。
固然你有其余的想法欢迎评论一块儿交流~
The end.