基于vue实现web端超大数据量表格

11,301 阅读12分钟


一、整体思路

 1.思路来源

      最近工作比较忙好久没写文章了,有一丢丢不知道如何写起了,那就先说说我是为什么要开发本文的组件吧。公司有一个定位系统,基本上来说一个定位单位一分钟或者更短就会有一个定位点,这样一天下来就会有很多定位点了,如果表格想要一下子放一天甚至三天的数据,那么数据量将会特别大(可能会到达5万条左右的数据),如果我们显示的列又比较多的话,那么表格的卡顿问题就会很明显了。我们公司web端选择的ui框架是iview ,说实话iview的其他组件还行,不过表格的话在大量数据面前显得很疲软,反而我以前使用的easyui之类的老框架的表格性能和功能上都很好,毕竟它们已经经历了很多优化,表格这个组件的拓展性很大,想要在性能和功能上都做好十分的困难。

    easyui是个与时俱进的框架,有一次我点开它的官网发现它已经出了基于现在热门的vue、react、angular的ui组件。于是我这次选择去看看它基于vue的表格,于是我看到了这个组件附上连接www.jeasyui.net/demo_vue/68…。我发现它通过分页延迟加载的方法解决了大数据量卡断的问题,这是我基本能够理解的,不过看完之后我有一些疑问,首先如果他只渲染了一部分数据,在滚动条滚动的时候再加载数据,那么为什么滚动条为什么一直是那么长。机智的我打开了开发者模式查看了表格部分的html代码


一看我明白了,图中的表格底部和表格顶部部分就是滚动条高度一直不变的原因,而中间部分根据滚动条的滚动始终只加载40条数据,这样大数据量的表格卡顿问题就解决了

2.思路确认

那么思路我们基本上可以有了,我们来理一下。

  • 首先我们可以认为这个表格分为3个部分[表格顶部:(top)、表格滚动区域:(screen)、表格底部:(bottom)]。
  • topbottom部分的问题是高度的计算,基本上可以确定应该再滚动条滚动的时候,得到滚动的位置,然后根据总数据量和screen部分的数据量计算出topbottom的高度,想到这里我脑海里就出现了几个字(计算属性)用在这里应该再合适不过了。
  • screen部分的实现心中的初步想法是根据滚动高度计算应该加载的数据。不过如何做到在过度数据的时候更加流畅,心中还有一些些疑惑于是我继续观察了它的html。为了更好的表述,前端达芬奇打开了他的画图软件开始了作画(●'◡'●)
首先我们刚刚提到了screen部分始终显示40条数据,所以我们通过滚动事件判断当页面滚动到超过screenbottom部分的底部的时候我们就向下加载20条数据同时删除screentop部分的数据这样用户使用的时候不会出现向下滚动加载然后轻微上移又要加载的情况。看到这里很多人肯定在想如果这个用户是个皮皮怪,拉着滚动条疯狂拖动怎么办,那我们就再来看一张图片(●'◡'●)


如果皮皮怪们将滚动条滚到了大于本来待加载20条数据高度的位置,我们就用新的处理方式删除所有的40条数据,根据滚动的位置计算当前位置上下各20条的数据。再这个过程当中可能会出现表格变白一下的过程,不过我觉得应该可以通过遮罩层来处理。

    基本上的思路有了,那么我们开始实现它吧 (●'◡'●)。

二、实现过程

(首先我先说一下,我实现的这个表格并不是考虑的那么全面,开发的初衷只是解决卡断这个问题,表格排序多选之类的功能等之后再拓展)

1.表格的结构

表格通过2个table标签组成,第一个是表头第二个是数据内容,方便后期拓展。这里偷懒没有把表头和内部内容和tr再单独成一个组件让代码可读性更好之后还可以再优化。

2.逻辑实现

我们直接说最主要的逻辑部分,首先我们看看props和data部分

props: {
    loadNum: {
      //默认加载行数
      type: [Number, String],
      default() {
        return 20;
      }
    },
    tdHeight: {
      //表格行高
      type: [Number, String],
      default() {
        return 40;
      }
    },
    tableHeight: {
      //表格高度
      type: [Number, String],
      default() {
        return "200";
      }
    },
    tableList: {
      //所有表格数据
      type: Array,
      default() {
        return [];
      }
    },
    columns: {
      //所有表格匹配规则
      type: Array,
      default() {
        return [];
      }
    },
    showHeader: {
      type: Boolean,
      default: true
    }
  },
  data() {
    return {
      isScroll: 17,
      showLoad: false,
      columnsBottom: [], //实际渲染表格规则
      showTableList: [], //实际显示的表格数据
      loadedNum: 0, //实际渲染的数据数量
      dataTotal: 0, //总数据条数
      dataTop: 0, //渲染数据顶部的高度
      scrollTop: 0, //滚动上下的距离
      interval: null, //判断滚动是否停止的定时器
      scrollHeight: 0, //数据滚动的高度
      selectTr: -1 //选择的行
    };
  },

然后我们看看滚动事件应该做一些什么先上代码

    
  //滚动条滚动    handleScroll(event) {      let bottomScroll = document.getElementById("bottomDiv");      let topScroll = document.getElementById("topDiv");      if (bottomScroll.scrollTop > this.scrollTop) {        //记录上一次向下滚动的位置        this.scrollTop = bottomScroll.scrollTop;        //向下滚动        this.handleScrollBottom();              } else if (bottomScroll.scrollTop < this.scrollTop) {        //记录上一次向上滚动的位置        this.scrollTop = bottomScroll.scrollTop;        //向上滚动        this.handleScrollTop();      } else {        //左右滚动        this.handleScrollLeft(topScroll, bottomScroll);      }    }

首先我们通过scrollTop这个变量在每次进入滚动事件的时候记录垂直滚动条的位置,如果这个值不变那么这次滚动就是左右滚动,如果这个值变大看那么就是向下滚动,如果这个值变小了那么就是向上滚动。左右滚动的时候我们需要做的事情就是让表头随着内容一起移动,这样就可以达到左右移动表头动上下移动表头固定的效果。

   //滚动条左右滚动
    handleScrollLeft(topScroll, bottomScroll) {
      //顶部表头跟随底部滚动
      topScroll.scrollTo(bottomScroll.scrollLeft, topScroll.pageYOffset);
    },

如果是向上移动我们就要做我们在思路中提高的事情了先看代码

    //滚动条向上滚动
    handleScrollTop() {
      //如果加载的数据小于默认加载的数据量
      if (this.dataTotal > this.loadNum) {
        let computeHeight = this.dataTop; //数据需要处理的时候的高度
        if (
          this.scrollTop < computeHeight &&
          this.scrollTop >= computeHeight - this.loadNum * this.tdHeight
        ) {
          this.showLoad = true;
          //如果滚动高度到达数据显示顶部高度
          if (this.dataTotal > this.loadedNum) {
            //如果数据总数大于已经渲染的数据
            if (this.dataTotal - this.loadedNum >= this.loadNum) {
              //如果数据总数减去已经渲染的数据大于等于loadNum
              this.dataProcessing(
                this.loadNum,
                this.loadedNum - this.loadNum,
                "top"
              );
            } else {
              this.dataProcessing(
                this.dataTotal - this.loadedNum,
                this.dataTotal - this.loadedNum,
                "top"
              );
            }
          }
        } else if (
          this.scrollTop <
          computeHeight - this.loadNum * this.tdHeight
        ) {
          this.showLoad = true;
          let scrollNum = parseInt(this.scrollTop / this.tdHeight); //滚动的位置在第几条数据
          if (scrollNum - this.loadNum >= 0) {
            this.dataProcessing(this.loadNum * 2, scrollNum, "topAll");
          } else {
            this.dataProcessing(scrollNum + this.loadNum, scrollNum, "topAll");
          }
        }
      }
    },

  1. 首先我们判断加载的数据是否小于默认加载的数据量,如果时那么就不需要做任何逻辑了,因为已经加载了所有的数据了。
  2. 判断滚动高度是不是已经超过了当前screen部分数据的顶部位置并且小于当前screen部分数据的顶部位置减去默认加载数据量的高度,也就是我们之前提到第一种情况,那么大于当前screen部分数据的顶部位置减去默认加载数据量的高度就是第二种情况了。
  3. 如果进入2个判断this.showLoad设置为true,将遮罩层打开,避免表格变白影响用户的体验,提示在加载。
  4. 第一种情况如果数据顶部小于默认加载数据,我们只加载剩余高度的数据如果大于则加载默认加载的this.loadNum数量的数据
  5. 第二种情况也是一样判断只不过判断this.loadNum*2是否大于数据顶部的数据条数,只加载剩余高度的数据或者加载this.loadNum*2数量的数据。
向下滚动其实是一样的思路我们看一下代码

    //滚动条向下滚动
    handleScrollBottom() {
      let computeHeight =
        this.dataTop +
        this.loadedNum * this.tdHeight -
        (this.tableHeight - this.tdHeight - 3); //数据需要处理的时候的高度
      if (
        this.scrollTop > computeHeight &&
        this.scrollTop <= computeHeight + this.tdHeight * this.loadNum
      ) {
        this.showLoad = true;
        //如果滚动高度到达数据显示底部高度
        if (this.dataTotal > this.loadedNum) {
          //如果数据总数大于已经渲染的数据
          if (this.dataTotal - this.loadedNum >= this.loadNum) {
            //如果数据总数减去已经渲染的数据大于等于20
            this.dataProcessing(
              this.loadedNum - this.loadNum,
              this.loadNum,
              "bottom"
            );
          } else {
            this.dataProcessing(
              this.dataTotal - this.loadedNum,
              this.dataTotal - this.loadedNum,
              "bottom"
            );
          }
        }
      } else if (
        this.scrollTop >
        computeHeight + this.tdHeight * this.loadNum
      ) {
        this.showLoad = true;
        let scrollNum = parseInt(this.scrollTop / this.tdHeight); //滚动的位置在第几条数据
        if (scrollNum + this.loadNum <= this.dataTotal) {
          this.dataProcessing(scrollNum, this.loadNum * 2, "bottomAll");
        } else {
          this.dataProcessing(
            scrollNum,
            this.dataTotal - scrollNum + this.loadNum,
            "bottomAll"
          );
        }
      }
    },

计算了好了有4种情况,并且计算出了对应需要删除和新增的数据量。我们来看看dataProcessing这个函数做了什么事情。

   //上下滚动时数据处理
    dataProcessing(topNum, bottomNum, type) {
      let topPosition = parseInt(this.dataTop / this.tdHeight);
      if (type === "top") {
        this.showTableList.splice(this.loadedNum - bottomNum, bottomNum); //减去底部数据
        for (var i = 1; i <= topNum; i++) {
          //加上顶部数据
          let indexNum = topPosition - i;
          this.tableList[indexNum].index = indexNum + 1;
          this.showTableList.unshift(this.tableList[indexNum]);
        }
        this.loadedNum = this.loadedNum + topNum - bottomNum; //重新计算实际渲染数据条数
        this.dataTop = this.dataTop - topNum * this.tdHeight; //重新计算渲染数据的高度
        document.getElementById("bottomDiv").scrollTop =
          document.getElementById("bottomDiv").scrollTop +
          bottomNum * this.tdHeight;
        this.scrollTop = document.getElementById("bottomDiv").scrollTop;
      } else if (type == "bottom") {
        this.showTableList.splice(0, topNum); //减去顶部数据
        for (var i = 0; i < bottomNum; i++) {
          //加上底部数据
          let indexNum = topPosition + this.loadedNum + i;
          this.tableList[indexNum].index = indexNum + 1;
          this.showTableList.push(this.tableList[indexNum]);
        }
        this.loadedNum = this.loadedNum - topNum + bottomNum; //重新计算实际渲染数据条数
        this.dataTop = this.dataTop + topNum * this.tdHeight; //重新计算渲染数据的高度
        document.getElementById("bottomDiv").scrollTop =
          document.getElementById("bottomDiv").scrollTop -
          topNum * this.tdHeight;
        this.scrollTop = document.getElementById("bottomDiv").scrollTop;
      } else if (type == "bottomAll") {
        this.showTableList = []; //减去顶部数据
        let scrollNum = topNum;
        for (var i = 0; i < bottomNum; i++) {
          //加上底部数据
          let indexNum = scrollNum - this.loadNum + i;
          this.tableList[indexNum].index = indexNum + 1;
          this.showTableList.push(this.tableList[indexNum]);
        }
        this.loadedNum = bottomNum; //重新计算实际渲染数据条数
        this.dataTop = (scrollNum - this.loadNum) * this.tdHeight; //重新计算渲染数据的高度
        this.scrollTop = document.getElementById("bottomDiv").scrollTop;
      } else if (type == "topAll") {
        this.showTableList = []; //减去顶部数据
        let scrollNum = bottomNum;
        for (var i = 0; i < topNum; i++) {
          //加上底部数据
          let indexNum = scrollNum - topNum + this.loadNum + i;
          this.tableList[indexNum].index = indexNum + 1;
          this.showTableList.push(this.tableList[indexNum]);
        }
        this.loadedNum = topNum; //重新计算实际渲染数据条数
        this.dataTop = (scrollNum - topNum + this.loadNum) * this.tdHeight; //重新计算渲染数据的高度
        this.scrollTop = document.getElementById("bottomDiv").scrollTop;
      }
      this.showLoad = false;
    },

  1. 首先先删除我们之前计算好的应该删除的数据我们用splice方法删除对应的数据,然后通过一个简单的for循环,如果是向上滚动应该将数据加在顶部我们用unshift方法,如果是向下滚动我们应该加在底部我们用push方法。
  2. 处理好数据以后我们还需要重新计算实际渲染数据条数,将loadedNum的值改为现在显示的数据条数
  3. 重新计算渲染数据的高度,计算出dataTop现在显示的数据顶部的高度
  4. 因为topbottom的变化会导致表格scrollTop的值出现变化,这个时候我们就要动态把滚动条移动到正确的位置

最后我们来说说之前考虑的topbottom,一开始我们就想好了应该用计算属性去做,事实也说明的确这样,我们看看代码

    computed: {
    tableOtherTop() {
      //表格剩余数据顶部高度
      return this.dataTop;
    },
    tableOtherBottom() {
      //表格剩余数据底部高度
      return (
        this.dataTotal * this.tdHeight -
        this.dataTop -
        this.loadedNum * this.tdHeight
      );
    }
  },

这样就能保证topbottom高度的变化能够触发表格的变化。

top的高度应该就是显示数据顶部的高度(dataTop)。

bottom的高度应该就是数据的总高度-显示的数据的高度(this.loadedNum * this.tdHeight)-top的高度。

最后我们来看看效果图


2020-3-26更新

  • 通过函数防抖解决拖动过程出现闪烁,滚动条跳动的情况。同时在每次触发滚动的时候进行友好判断 判断当前滚动是否超出数据范围超出显示加载中动画。

    /**
         * @typedef {Object} Options -配置项
         * @property {Boolean} leading -开始是否需要额外触发一次
         * @property {this} context -上下文
         **/
        //使用Proxy实现函数防抖
        proxy(
          func,
          time,
          options = {
            leading: true,
            context: null
          }
        ) {
          let timer;
          let _this = this;
          let handler = {
            apply(target, _, args) {
              //代理函数调用
              let bottomScroll = document.getElementById("bottomDiv");
              let topScroll = document.getElementById("topDiv");
              if (bottomScroll.scrollTop == _this.scrollTop) {
                //左右滚动
                _this.handleScrollLeft(topScroll, bottomScroll);
                return;
              }
              // 和闭包实现核心逻辑相同
              if (!options.leading) {
                if (timer) return;
                timer = setTimeout(() => {
                  timer = null;
                  Reflect.apply(func, options.context, args);
                }, time);
              } else {
                if (timer) {
                  _this.needLoad(bottomScroll);
                  clearTimeout(timer);
                }
                timer = setTimeout(() => {
                  Reflect.apply(func, options.context, args);
                }, time);
              }
            }
          };
          return new Proxy(func, handler);
        },
        //是否显示加载中
        needLoad(bottomScroll) {
          if (
            Math.abs(bottomScroll.scrollTop - this.scrollTop) >
            this.tdHeight * this.loadNum
          ) {
            this.showLoad = true; //显示加载中
            this.scrollTop = bottomScroll.scrollTop;
          }
        }

  • 监听表格高度变化 解决高度变化导致部分内容未渲染的bug
  • 这个表格组件已经发布到npm上欢迎大家使用,提问题

总结

这个组件的开发最麻烦的地方就是理清楚各种情况,然后写好各种计算保证不出错。开发的过程中我也有一种想要自己开发个简易table组件的想法,无奈感觉个人水平有限,不过我也在很多地方做了伏笔,等以后有时间再来拓展这个组件,加油~~~///(^v^)\\\~~~。

这里附上我的github地址github.com/github30789…,我把项目已经上传上去了,如果喜欢可以给我个start,谢谢(●'◡'●),可能其中还存在很多问题,也希望能够得到各位大佬的指点。