当前位置:   article > 正文

【Element-ui】el-table大数据量渲染卡顿问题_页面表格数据都加载完

页面表格数据都加载完

1、场景描述

在项目开发中,遇到在表格中一次性加载完的需求,且加载数量不少,有几百几千条,并且每条都可能有自己的下拉框,输入框来做编辑功能,此时普通的el-table肯定会导致浏览器卡死,那么怎么办呢?

2、解决方案

当然很多童鞋肯定会想到利用插件,其实我本人是不咋喜欢插件的,能自己写就自己写,毕竟插件可能也有bug或者啥的,万一出现了,作者不去改咋办,所以我总结了下面几个解决方法

提示:本篇博客基本都用的tsx + vue3 composition-api体验版 来写的噢,用vue2或者vue3模板语法写会更简单噢,照葫芦画瓢逻辑都是一样的,我这里就不写了,想了解相关的vue3知识我另一篇博客上有噢~

Vue3知识点学习

1、滚动触底分页(良)

当纵向滚动条触底的时候,加载新的数据到当前表格中,逻辑如下:

table.scrollTop + table.clientHeight === table.scrollHeight 

当上述成立时候触发加载,table为表格dom, 但是如果数据很多的话,每次滚动都会将新的tr加入到表格中,那表格tr dom总数还是会依次递增,dom一多照样卡死

2、滚动区间分页(良+)

如果你没有表格编辑功能,全是展示数据的话,那么这个解决方案已经完全可以了

比如你当前表格可显示区域能够展示十条数据,那么首次进来时,显示数据的区间为 [0, 9],每次表格滚动时,都动态的去获取当前展示的区间,其实这种方式是比较好的,为啥呢?因为不管你几千几万条数据,我同一时刻就只有10行,完全不会因为tr数量过多导致浏览器渲染卡顿,当然这可视区域能展示多少条数据,是有你表格可视高度和单行tr高度一起决定的

  1. const selectWrap = table.querySelector('.el-table__body-wrapper');
  2. const selectRow = table.querySelector('table tr');
  3. 展示tr数量 = Math.ceil(selectWrap.clientHeight / selectRow.clientHeight)

tsx:页面

  1. setup() {
  2.   return () => (
  3.     <el-table
  4.       data={visibleResult.value} // 可视区域的数据
  5.     >
  6.     </el-table>
  7.   )
  8. }

ts:逻辑

  1. /** 表格上展示的数据 */
  2. const visibleResult = computed(() => {
  3. return result.value.filter((_item, index) => {
  4. if (index < curStartIndex.value) {
  5. return false;
  6. } else if (index > curEndIndex.value) {
  7. return false;
  8. } else {
  9. return true;
  10. }
  11. });
  12. })

那么如何去控制这个滚动区间呢?这个方法很多,监听滚动条的scroll,当向下滚动或向上滚动,我们都可以监听到,然后改变curStartIndex和curEndIndex的值就可以改变啦,这样的话,光是只看数据倒是解决了,但是要是表格要实现编辑效果咋办?每行数据有十几个下拉框和输入框,你要知道,el-select dom层级很高,像这种el-select数量一多,就算你当前展示区域没有多少条数据,也会导致渲染卡顿的,所以就有下面的优化版方案

3、滚动区间分页 + 表格编辑(优)

当前表格可视区域有30条数据,每条数据有10个el-select和5个el-input(当然可能有童鞋会说,el-select dom层级高,那我就自己写select鸭~但是你自己写的未必有el-select好看且功能相当)。此时首次加载就很卡顿了,原因就是el-select多了,那么我首先想的是这些el-select又不是直接要用,为啥不在我点击这个单元格时候再弹出下拉呢?不点击单元格时候,就直接展示文本效果(el-select没有值时展示 '请选择', 有值时展示你选择的值)

例图如下:

 滚动区间分页

 不管你有多少条数据,我始终首次只加载这么十多个tr,tr数量不变,变化的是每次的区间取值,大家会发现下面有个tr id为 virtual-scroll,那这是干什么 的呢?其实核心思路是:

  1. 显示区的高度 + 已经滚动过的高度 + 虚拟滚动条高度 === 总的数据高度
  2. table-wrapper.clientHeight + table-wrapper.scrollTop + virtual-scroll.height === data.lentth * tr.clientHeight

 是不是发现和方案1类似,但是区别不同的是,虚拟条高度可是一来就会被计算出来的,因为开始时候scrollTop为0, 那么 虚拟条高度 就是 总的数据高度 - 显示区高度

tsx: 通过自定义指令来监听表格的滚动,返回值触发loadMore方法,来决定显示区展示数据的区间

  1. setup() {
  2. return () => (
  3. <el-table
  4. data={visibleResult.value} // 可视区域的数据
  5. {...{ directives: [{ name: 'load-more', value: methods.loadMore }] }}
  6. >
  7. </el-table>
  8. )
  9. }

ts: 逻辑

  1. /** 表格上展示的数据 */
  2. const visibleResult = computed(() => {
  3. return result.value.filter((_item, index) => {
  4. if (index < curStartIndex.value) {
  5. return false;
  6. } else if (index > curEndIndex.value) {
  7. return false;
  8. } else {
  9. return true;
  10. }
  11. });
  12. })
  13. const methods = {
  14. /**
  15. * 懒加载回调
  16. * @param startIndex 区段位置开始索引
  17. * @param endIndex 区段位置结束索引
  18. */
  19. loadMore(startIndex: number, endIndex: number) {
  20. curStartIndex.value = startIndex
  21. curEndIndex.value = endIndex
  22. },
  23. }

那上面是实现滚动区间的代码,那说的表格编辑相关在哪里呢?下面即是:

表格编辑

当我们表格中有很多下拉框和输入框时,其实拖垮性能的多半是el-select,那解决方法是将渲染input和select的逻辑提出来,input不做处理,select当前单元格row,column索引和focusCell的值一致时说明单元格被聚焦了,就显示el-select,否则直接展示文本

关键方法及属性讲解:

inputChange:输入框回调

selectChange:下拉框选择回调

selectPerofrmance:下拉框当前值渲染

domPropsInnerHTML: 等同于v-html,但是jsx中不能用v-html

decorateHeader:表格各列数据我都是动态配置的,后续我会出一期博客来讲解vue3+tsx下如何封装表格

然后效果大致如下,总结下就是同一时间最多只会存在一个el-select,既然el-select dom减少了,那么表格渲染速度就自然而然快了

 上面例图中被黄色圈中的就是聚焦,没有被聚焦的都展示文本,源码讲解如下:

const focusCell = ref<string>('0,0') // 表格数据第0行第0列

 你在el-table 里有 cell-click 这个事件,它会将当前row, column全都回调回去,那么你就会知道你当前点击单元格的索引值, 我们将索引值生成, 在同一时间,focusCell只会有一条数据,所以我们直接用字符串来存储就好

  1. <el-table
  2.   on-cell-click={methods.cellClick}
  3. >
  4. </el-table>
  5. const methods = {
  6.   cellClick(row, column) {
  7.     if (focusCell.value !== `${row.index},${column.index}`) {
  8.         focusCell.value = `${row.index},${column.index}`
  9.       }
  10.   }
  11. }

那么关键来了,下拉框单元格聚焦的时候,我们才显示下拉框,其他时候展示文本,输入框类型单元格聚焦的时候不做处理,代码该如何写呢?

  1. setup() {
  2. return () => {
  3. /** 输入框类型渲染 */
  4. const inputDomRender = (scope, item) => (
  5. <el-input
  6. value={scope.row[item.prop]}
  7. on-input={e => methods.inputChange(e, scope, item)}
  8. />
  9. )
  10. /** 下拉框类型渲染 */
  11. const selectDomRender = (scope, item) => (
  12. (focusCell.value === `${scope.row.autoIndex},${scope.column.index}` ? <el-select
  13. value-key='id'
  14. value={scope.row[item.prop]}
  15. onChange={e => methods.selectChange(e, scope, item)}
  16. >
  17. {
  18. item.selects.map(item1 => {
  19. return <el-option
  20. key={item1.id}
  21. label={item1.label}
  22. value={item1.id}
  23. >
  24. </el-option>
  25. })
  26. }
  27. </el-select> : <div
  28. domPropsInnerHTML={methods.selectPerofrmance(scope.row[item.prop], item.prop)}></div>)
  29. )
  30. return <el-table
  31. on-cell-click={methods.cellClick}
  32. >
  33. {
  34. decorateHeader.map((item: TableLabel) => {
  35. return <el-table-column
  36. width={item.width}
  37. label={item.label}
  38. align={item.align ? item.align : 'center'}
  39. prop={item.prop}
  40. scopedSlots={{
  41. default: scope => {
  42. return <div
  43. >
  44. {
  45. item.mode !== 'input' ? selectDomRender(scope, item) : inputDomRender(scope, item)
  46. }
  47. </div>
  48. }
  49. }}
  50. >
  51. </el-table-column>
  52. })
  53. }
  54. </el-table>
  55. }
  56. }

自此上述两步优化,其实就能使我们一次性加载几千条可编辑数据不会卡顿了~

3、v-load-more自定义指令

源码:

  1. import {
  2. VNodeDirective
  3. } from 'vue'
  4. let timeout;
  5. /** 设置表格滚动区间 */
  6. const setRowScrollArea = (topNum, showRowNum, binding) => {
  7. if (timeout) {
  8. clearTimeout(timeout);
  9. }
  10. timeout = setTimeout(() => {
  11. binding.value.call(null, topNum, topNum + showRowNum);
  12. });
  13. };
  14. const loadMore= {
  15. bind(el: Element, _binding) {
  16. setTimeout(() => {
  17. // 创建虚拟滚动条
  18. const selectWrap = el.querySelector('.el-table__body-wrapper');
  19. const selectTbody = selectWrap.querySelector('table tbody');
  20. const createElementTR = document.createElement('tr');
  21. createElementTR.id = 'virtual-scroll'
  22. selectTbody.append(createElementTR); // 先行将虚拟滚动条加入进来
  23. })
  24. },
  25. componentUpdated(el: Element, binding: VNodeDirective, vnode, oldVnode) {
  26. setTimeout(() => {
  27. const dataSize = vnode.data.attrs['data-size'];
  28. const oldDataSize = oldVnode.data.attrs['data-size'];
  29. // 当数量相同时,表明当前未发生更新,减少后续操作
  30. if (dataSize === oldDataSize) {
  31. return;
  32. }
  33. const selectWrap = el.querySelector('.el-table__body-wrapper');
  34. const selectTbody = selectWrap.querySelector('table tbody');
  35. const selectRow = selectWrap.querySelector('table tr');
  36. // 当一行都没有,说明无数据渲染,但一般逻辑都不会进入这里
  37. if (!selectRow) {
  38. return;
  39. }
  40. const rowHeight = selectRow.clientHeight;
  41. // 能够在当前显示区的展示条数,本项目就是11条
  42. const showRowNum = Math.round(selectWrap.clientHeight / rowHeight);
  43. const createElementTRHeight = (dataSize - showRowNum) * rowHeight;
  44. const createElementTR = selectTbody.querySelector('#virtual-scroll')
  45. // 监听滚动后事件
  46. selectWrap.addEventListener('scroll', function() {
  47. let topPx = this.scrollTop;
  48. let topNum = Math.round(topPx / rowHeight);
  49. const minTopNum = dataSize - showRowNum;
  50. if (topNum > minTopNum) {
  51. topNum = minTopNum;
  52. }
  53. if (topNum < 0) {
  54. topNum = 0;
  55. topPx = 0;
  56. }
  57. selectTbody.setAttribute('style', `transform: translateY(${topPx}px)`);
  58. // 本来触底的话,应该设置为0,但是触底后 就没有滚动条了
  59. createElementTR.setAttribute('style', `height: ${createElementTRHeight - topPx > 0 ? createElementTRHeight - topPx : rowHeight}px;`);
  60. setRowScrollArea(topNum, showRowNum, binding);
  61. })
  62. });
  63. }
  64. }
  65. export default loadMore

 不太了解自定义指令是啥的可以参考我另一篇博客

vue学习(6)自定义指令详解及常见自定义指令

4、有趣拓展

1、我想在上述表格中对指定列实现高亮搜索怎么做?

当有值时滚动到指定位置,无值时不动,那首先在加载数据时,要先写下面代码

  1. // 先让他触发滚动,才能让virtual-scroll高度生成
  2. table.$el.querySelector('.el-table__body-wrapper').scrollTo({ top: 1, behavior: 'smooth' })

输入框执行逻辑如下,防抖肯定是要的,然后搜索的列是 originName,当发现有搜索到值时,找到第一个被匹配到的行索引,然后去计算表格应该滚动到哪个高度位置,然后滚动

  1. /**
  2. * 滚动定位到表格指定位置
  3. * @param flag 是否能执行滚动的标志
  4. * @returns
  5. */
  6. scrollToTable: () => $debounce(function() {
  7. // 当没有值时,不进行搜索
  8. if (dialogSearchKey.value) {
  9. const vmEl = table.value.$el;
  10. const selectWrap = vmEl.querySelector('.el-table__body-wrapper')
  11. if (vmEl) {
  12. const autoIndex = result.value.find((item: TableDataItem) => {
  13. return item.originName.indexOf(dialogSearchKey.value) !== -1
  14. })?.autoIndex ?? -1
  15. if (autoIndex !== -1) {
  16. scrollToIndex(selectWrap, autoIndex)
  17. }
  18. }
  19. }
  20. }, 500, false),
  21. /**
  22. * 表格滚动到指定索引值行
  23. * @param selectWrap 表格dom
  24. * @param autoIndex 索引
  25. */
  26. scrollToIndex(selectWrap, autoIndex) {
  27. const showNum = 12 // 当显示条数小于次数时,不进行滚动操作
  28. const topPx = autoIndex * columnHeight.value
  29. if (autoIndex > showNum) {
  30. selectWrap.scrollTo({ top: topPx, behavior: 'smooth' })
  31. }
  32. }

 然后高亮被搜索文字源码如下:

  1. setup() {
  2. return () => {
  3. return <el-table
  4. on-cell-click={methods.cellClick}
  5. >
  6. {
  7. decorateHeader.map((item: TableLabel) => {
  8. return <el-table-column
  9. width={item.width}
  10. label={item.label}
  11. align={item.align ? item.align : 'center'}
  12. prop={item.prop}
  13. scopedSlots={{
  14. default: scope => {
  15. return <div
  16. >
  17. <div
  18. class='multiline'
  19. domPropsInnerHTML={methods.textRender(scope.row[item.prop], item.prop)}
  20. />
  21. </div>
  22. }
  23. }}
  24. >
  25. </el-table-column>
  26. })
  27. }
  28. </el-table>
  29. }
  30. }

 props.heightLight  为输入框的搜索关键词

  1. /**
  2. * 文字渲染
  3. * @param word 被渲染的文字
  4. * @param prop 属性名
  5. * @returns 替换后的渲染文字
  6. */
  7. textRender(word: string, prop: string): string {
  8. const reg = new RegExp(`${props.heightLight}`, 'ig')
  9. // 有搜索关键词 && 是否有该子字符串
  10. if (props.heightLight && word.indexOf(props.heightLight) !== -1) {
  11. return word.replace(reg, `<font color='red'>$&</font>`)
  12. // 正常返回的列
  13. } else {
  14. return word
  15. }
  16. }

---本篇还是相当实用,喜欢就一键三连吧~欢迎评论---

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/529733
推荐阅读
相关标签
  

闽ICP备14008679号