:::: MENU ::::

TalkingData's Blog

现在开始,用数据说话。

技术专栏 | DMap——实战Vue百万条数据渲染表格组件开发

cialis erfaring cialis i norge hva er kamagra cialis efeitos secundarios cialis bula viagra effekt viagra norge viagra på nett viagra nettbutikk viagra infarmed levitra comprimidos cialis uten resept cialis pris levitra eller cialis kamagra gel comprar viagra farmacia
  • May 03 / 2018
  • 0
Enterprise

技术专栏 | DMap——实战Vue百万条数据渲染表格组件开发

作者:TalkingData 李志刚

本文由TalkingData原创,转载请获取授权。

李志刚:近几个月在开发一个基于Vue的数据可视化分析辅助应用———DMap(谛听),一套为数据分析师和数据科学家提供的基于位置大数据分析的工具,旨在提高数据分析效率,降低获取多数据并行分析成本,简化大屏和数据报告开发制作流程。其UI组件使用的是iView,地图可视化库使用的是inMap,服务端使用Node.js搭建。

DMap的核心就是服务大数据分析,所以当面对几万几十万甚至百万级别的数据时,性能优化是一个具有挑战性的问题。今天我就拿项目中一个表格渲染的优化为例来展开介绍。

在前端开发中,用表格来展示数据是再平常不过的了,当数据量较多时,我们通常的做法是使用分页,如果数据量不算太多只有两三页,我们大可以把全量数据获取下来,在前端做简单的分页展示。当数据量再上一个等级时,我们就需要根据页数向服务端请求这一页需要的数据。但是DMap作为助力大数据可视化的分析工具,我们需要将全量的数据在前端做展示,而为了提升用户体验,我们在表格的展示上决定不做分页,也不做懒加载,而是像Excel那样可以无缝隙的滚动。

在Web中,长列表渲染的性能问题已经有一些成熟的方案,表格和长列表相似,当渲染的行数达到一定量时,滚动就会变得卡顿,所以我们使用了虚拟渲染的方案,就是只渲染用户所能看到的区域的一小部分数据,然后通过滚动来计算显示的数据,和上下占位元素的高度。

通过这个图可以对原理有个大概的了解,接下来说下计算上的细节。

首先我们需要监听表格外层容器(也就是显示滚动条的元素)的scroll事件,在scroll事件绑定的方法中我们只做一件事,那就是获取外层容器当前滚动了的高度scrollTop的值。我们的所有计算,包括三个表格位置的替换、表格数据的选取、上下占位元素的高度的计算都与scrollTop相关。

下面是scroll事件的绑定的方法:

handleScroll (e) {
      const ele = e.srcElement || e.target;
      const { scrollTop, scrollLeft } = ele;
      this.scrollLeft = scrollLeft;
      this.scrollTop = scrollTop;
    }

我们只需要在这里把scrollTop和scrollLeft的值赋给vue实例对应的值,然后我们用watch监听scrollTop的改变,如果更新了,就来计算当前处于可视区域的表格索引号currentIndex:

(注:左右滑动即可查看完整代码,下同)

this.currentIndex = parseInt((top % (this.moduleHeight * 3)) / this.moduleHeight);

这的top就是更新后的this.scrollTop的值,moduleHeight是单个表格的高度,我们称它为一个模块。

拿到currentIndex的值后,我们就可以计算三个表格的显示位置,和每个表格中填充的数据。三个表格我们是通过render函数渲染的,我们根据currentIndex的值来返回不同顺序的render函数:

getTables (h) {
let table1 = this.getItemTable(h, this.table1Data, 1);
let table2 = this.getItemTable(h, this.table2Data, 2);
let table3 = this.getItemTable(h, this.table3Data, 3);
if (this.currentIndex === 0) return [table1, table2, table3];
else if (this.currentIndex === 1) return [table2, table3, table1];
else return [table3, table1, table2];
}

数组中表格顺序不同,反应在页面上的效果就是不同的先后顺序。最后我们通过这个方法得到完整的render:

renderTable (h) {
      return h('div', {
        style: this.tableWidthStyles
      }, this.getTables(h));
    }

然后使用封装的无状态的组件,来渲染我们得到的表格render。

<render-dom :render="renderTable"></render-dom>

renderDom组件的实现如下:

export default {
  name: 'RenderCell',
  functional: true,
  props: {
    render: Function,
    backValue: [Number, Object]
  },
  render: (h, ctx) => {
    return ctx.props.render(h, ctx.props.backValue, ctx.parent);
  }
};

接下来我们讲下三个表格中填充的数据的计算。

我们按照三个模块都在可视区域经过一次算是一轮,通过scrollTop来和currentIndex来计算每个模块当前是在第几轮展示,但因为我们是从第二个表格才开始做这个逻辑的处理(为了轮播效果更平滑),所以要先判断当前滚动的高度是否大于一个模块的高度,如果大于才做如下计算:

switch (this.currentIndex) {
   case 0: t0 = parseInt(scrollTop / (this.moduleHeight * 3)); t1 = t2 = t0; break;
   case 1: t1 = parseInt((scrollTop - this.moduleHeight) / (this.moduleHeight * 3)); t0 = t1 + 1; t2 = t1; break;
   case 2: t2 = parseInt((scrollTop - this.moduleHeight * 2) / (this.moduleHeight * 3)); t0 = t1 = t2 + 1;
}

计算出每个模块在第几轮展示后,就可以来取对应的表格数据了:

const count1 = this.times0 * this.itemNum * 3;
this.table1Data = this.insideTableData.slice(count1, count1 + this.itemNum);
const count2 = this.times1 * this.itemNum * 3;
this.table2Data = this.insideTableData.slice(count2 + this.itemNum, count2 + this.itemNum * 2);
const count3 = this.times2 * this.itemNum * 3;
this.table3Data = this.insideTableData.slice(count3 + this.itemNum * 2, count3 + this.itemNum * 3);

到这里虚拟渲染的重要内容都介绍完了。表格开发完成后,在项目中实际使用时,当加载二十多万条数据来测试时,整个页面卡的让人无法忍受,数据量越大页面卡顿越严重。我们的表格是没有问题的,问题出在Vue帮了我们“倒忙”。

在Vue实例中添加的对象,Vue会先遍历一遍对象的所有属性,用——

Object.defineProperty()为每个对象创建对应的getter和setter。

而在项目中,我们的insideTableData只是一个数据集对象中的一个属性,这个对象还包括很多与这一个数据集相关的信息,我们在使用this.insideTableData.slice获取数据的时候会触发this.insideTableData对应的getter,从而执行一些其他逻辑,而我们的滚动又会频繁的(仅当currentIndex变化的时候)需要重新填充表格数据,所以这会造成卡顿。

解决这个问题的办法就是阻止Vue给我们的数据集对象设置对应的setter和getter,

我了解的有两种方法,一是文档中提到的:

我们使用的时候就需要通过——

this.$data._dataSet.insideTableData(这里的_dataSet就是一个数据集对象)来获取。

另一种方法,就是使用ES5的Object.preventExtensions在将数据集对象交给Vue实例代理前将对象密封,这样数据集对象就变成了不可拓展的了,Vue就不会再添加新的属性了,也就无法设置setter和getter了。

做了这个处理后渲染几十万数据跟玩儿似的流畅。但是阻止Vue设置getter和setter也造成了一些问题,比如原来表格组件中的一些依赖于表格数据的计算属性,现在无法在表格数据变化时重新计算,当然了,影响不大,就一个表格行数的计算,所以改成了手动设置这个值。

到这里要讲的差不多了,这只是项目中的一点优化内容,我封装的vue-bigdata-table(没办法,好名字都被注册了)表格组件不仅仅这点功能,目前还包括拖动修改列宽、固定列不横向滚动,固定表头、内置排序、编辑单元格、粘贴、筛选、自定义表头和单元格等功能。现在也已经开源了,但是还有很多功能还在开发中。

Leave a comment

随时欢迎您 联系我们