后端返回十万条数据,前端该如何解决呢?

这个问题看起来非常的无厘头,但是考察了候选人的一个知识的储备情况,并且在实际开发的场景中很难遇到。

那么这个问题考察了一些什么内容呢?

  • 考察前端如何处理大量数据
  • 考察候选人对于大量数据的性能优化

所以,下面就让我来和大家一起,看看可以使用哪些方法优雅的处理这些数据吧

模拟出十万条数据

这里我们可以使用express来创建这么一个接口,代码如下

js 复制代码
route.get("/bigData", (req, res) => {
  res.header('Access-Control-Allow-Origin', '*'); // 允许跨域
  let arr = [] // 定义数组,存放十万条数据
  for (let i = 0; i < 100000; i++) { // 循环添加十万条数据
    arr.push({
      id: i + 1,
      name: '名字' + (i + 1),
      value: i + 1,
    })
  }
  res.send({ code: 0, msg: '成功', data: arr }) // 将十万条数据返回之
})

通过点击按钮发送请求

这个操作就很简单了,给按钮绑定一个点击事件,发送请求就完事了

js 复制代码
<el-button :loading="loading" @click="onClick">点击请求加载</el-button>

<el-table :data="arr">
  <el-table-column type="index" label="序" />
  <el-table-column prop="id" label="ID" />
  <el-table-column prop="name" label="名字" />
  <el-table-column prop="value" label="对应值" />
</el-table>

data() {
    return {
      arr: [],
      loading: false,
    };
},

async onClick() {
    // 发请求,拿数据,赋值给arr
}

好的,现在前置工作已经准备就绪,接下来,就是开始处理这个十万条数据了

方案一:直接渲染

当然,这种方法是不取的,因为渲染之后,页面打开就会直接卡死

js 复制代码
 async onClick() {
      this.loading = true;
      const res = await axios.get("http://ashuai.work:10000/bigData");
      this.arr = res.data.data;
      this.loading = false;
}

方案二:使用定时器分批次的渲染(定时加载,分堆思想)

正常来说,十万条数据请求,需要2秒到10秒之间(有可能更长,取决于数据具体内容),而这种方式就是,前端请求到10万条数据以后,先不着急渲染,先将10万条数据分堆分批次 。比如一堆存放10条数据,那么十万条数据就有一万堆,然后使用定时器,一次渲染一堆,渲染一万次即可,这样做的话,页面就不会卡死了。

写一个分批分组分堆的函数

js 复制代码
function averageFn(arr) {
  let i = 0; // 1. 从第0个开始截取
  let result = []; // 2. 定义结果,结果是二维数组
  while (i < arr.length) { // 6. 当索引等于或者大于总长度时,即截取完毕
    // 3. 从原始数组的第一项开始遍历
    result.push(arr.slice(i, i + 10)); // 4. 在原有十万条数据上,一次截取10个用于分堆
    i = i + 10; // 5. 这10条数据截取完,再截取下十条数据,以此类推
  }
  return result; // 7. 最后把结果丢出去即可
}

所谓的分堆思想就是一次截取一段一定长度的数据,比如截取十条数据,就是0-9一段,然后10-19,按这个顺序截取。假设我们的数据为 [1,2,3,4,5,6,7],分堆之后就变成了一个二维的数组,就是 [ [1,2,3], [4,5,6], [7]],然后呢,我们通过创建定时器,来进行一个定时的渲染。

定时渲染

js 复制代码
  async onClick() {
      this.loading = true;
      const res = await axios.get("http://ashuai.work:10000/bigData");
      this.loading = false;
      let twoDArr = averageFn(res.data.data);
      for (let i = 0; i < twoDArr.length; i++) {
        // 相当于在很短的时间内创建许多个定时任务去处理
        setTimeout(() => {
          this.arr = [...this.arr, ...twoDArr[i]]; // 赋值渲染
        }, 1000 * i); // 17 * i // 注意设定的时间间隔... 17 = 1000 / 60
      }
    },

方案三:使用requestAnimationFrame替代定时器去做渲染

关于 requestAnimationFrame(callback) 它是 window 下面的一个方法,跟setTimeout用法类似,接收一个回调函数,该回调函数会在浏览器下一次重绘之前执行。只不过 requestAnimationFrame 的刷新时间间隔是根据浏览器自己来定义的,浏览器为了保证流畅性,重绘频率会与系统刷新率保持一致,如果刷新率是60hz,即表示每秒刷新60次,也就是每次重绘的间隔时间大概是1000ms/60=16.6msrequestAnimationFrame会跟随浏览器的频率,每16.6ms回调函数就被执行一次,从而避免丢帧卡顿现象。

下面看代码

js 复制代码
async onClick() {
  this.loading = true;
  const res = await axios.get("http://ashuai.work:10000/bigData");
  this.loading = false;
  // 1. 将大数据量分堆
  let twoDArr = averageFn(res.data.data);
  // 2. 定义一个函数,专门用来做赋值渲染(使用二维数组中的每一项)
  const use2DArrItem = (page) => {
    // 4. 从第一项,取到最后一项
    if (page > twoDArr.length - 1) {
      console.log("每一项都获取完了");
      return;
    }
    // 5. 使用请求动画帧的方式
    requestAnimationFrame(() => {
      // 6. 取出一项,就拼接一项(concat也行)
      this.arr = [...this.arr, ...twoDArr[page]];
      // 7. 这一项搞定,继续下一项
      page = page + 1;
      // 8. 直至完毕(递归调用,注意结束条件)
      use2DArrItem(page);
    });
  };
  // 3. 从二维数组中的第一项,第一堆开始获取并渲染(数组的第一项即索引为0)
  use2DArrItem(0); 
},

方案四:搭配分页组件,实现前端分页效果

这个的话就是,咱们直接调用组件库里面的分页组件,进行一个分页的展示,这里我使用的是element-ui组件库,代码如下

js 复制代码
<template>
  <div class="box">
    <el-table height="240" :data="showTableData" border style="width: 100%">
      <el-table-column prop="name" label="姓名" width="180"></el-table-column>
      <el-table-column prop="age" label="年龄" width="180"></el-table-column>
      <el-table-column prop="home" label="家乡"></el-table-column>
    </el-table>
    <el-pagination
      layout="total, sizes, prev, pager, next, jumper"
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="pageIndex"
      :page-size="pageSize"
      :page-sizes="[2, 4, 6, 10]"
      :total="total"
    >
    </el-pagination>
  </div>
</template>
<script>
export default {
  data() {
    return {
      pageIndex: 1, // 第几页
      pageSize: 4, // 每页几条数据
      total: 0, // 总条目数
      allTableData: [], // 所有的数据
      showTableData: [], // 当前展示的数据
    };
  },
  mounted() {
    setTimeout(() => {
      // 1. 模拟发请求获取所有的数据
      let apiAllTableData = [
        {
          name: "孙悟空",
          age: 500,
          home: "花果山水帘洞",
        },
        {
          name: "猪八戒",
          age: 88,
          home: "高老庄",
        },
        {
          name: "沙和尚",
          age: 1000,
          home: "通天河",
        },
        {
          name: "唐僧",
          age: 9999,
          home: "东土大唐",
        },
        {
          name: "白龙马",
          age: 12,
          home: "东海",
        },
        {
          name: "观音菩萨",
          age: 18,
          home: "南海",
        },
        {
          name: "玉皇大帝",
          age: 123456789,
          home: "凌霄宝殿",
        },
        {
          name: "如来佛祖",
          age: 9999999.999,
          home: "迦毗罗卫国",
        },
      ];
      // 2. 存一份所有的数据
      this.allTableData = apiAllTableData;
      // 3. 获取总条目数
      this.total = this.allTableData.length;
      // 4. 根据当前是第几页、每页展示几条,去截取需要展示的数据
      this.getShowTableData();
    }, 1000);
  },
  methods: {
    getShowTableData() {
      // 5. 获取截取开始索引
      let begin = (this.pageIndex - 1) * this.pageSize;
      // 6. 获取截取结束索引
      let end = this.pageIndex * this.pageSize;
      // 7. 通过索引去截取,从而展示
      this.showTableData = this.allTableData.slice(begin, end);
    },
    // 8. 页数改变,重新截取
    handleCurrentChange(val) {
      this.pageIndex = val;
      this.getShowTableData();
    },
    // 9. 条目数改变,也重新截取
    handleSizeChange(val) {
      this.pageIndex = 1;
      this.pageSize = val;
      this.getShowTableData();
    },
  },
};
</script>

方案五:根据滚动触底加载,实现数据渲染

这里重点就是我们需要去判断,何时滚动条触底。判断方式主要有两种

  • scrollTop + clientHeight >= innerHeight
  • new MutationObserver()去观测

目前市面上主流的一些插件的原理,大致是这两种,这里可以直接使用 vue-scroller,这个插件实现。

在el-table中使用el-table-infinite-scroll指令步骤

安装,注意版本号(区分vue2和vue3)

cnpm install --save el-table-infinite-scroll@1.0.10

注册使用指令插件

js 复制代码
// 使用无限滚动插件
import elTableInfiniteScroll from 'el-table-infinite-scroll';
Vue.use(elTableInfiniteScroll);

因为是一个自定义指令,所以直接写在el-table标签上即可

js 复制代码
<el-table
  v-el-table-infinite-scroll="load"
  :data="tableData"
>
  <el-table-column prop="id" label="ID"></el-table-column>
  <el-table-column prop="name" label="名字"></el-table-column>
</el-table>

async load() {
    // 触底加载,展示数据...
}

方案六:使用无限加载/虚拟列表进行展示

什么是虚拟列表

所谓的虚拟列表实际上是前端障眼法 的一种表现形式。看到的好像所有的数据都渲染了,实际上只渲染可视区域的部分罢了,有点像我们看电影,我们看的话,是在一块电影屏幕上,一秒一秒的看(不停的放映),但是实际上电影有俩小时,如果把两个小时的电影都铺开的话,那得需要多少块电影屏幕呢?

同理,如果10万条数据都渲染,那得需要多少dom节点元素呢?所以我们只给用户看,他当下能看到的如果用户要快进或快退(下拉滚动条或者上拉滚动条),再把对应的内容呈现在电影屏幕上(呈现在可视区域内),这样就实现了看着像是所有的dom元素每一条数据都有渲染的障眼法效果了

实现一个简单的虚拟列表

html 复制代码
<template>
  <!-- 虚拟列表容器,类似"窗口",窗口的高度取决于一次展示几条数据
            比如窗口只能看到10条数据,一条40像素,10条400像素
            故,窗口的高度为400像素,注意要开定位和滚动条 -->
  <div
    class="virtualListWrap"
    ref="virtualListWrap"
    @scroll="handleScroll"
    :style="{ height: itemHeight * count + 'px' }"
  >
    <!-- 占位dom元素,其高度为所有的数据的总高度 -->
    <div
      class="placeholderDom"
      :style="{ height: allListData.length * itemHeight + 'px' }"
    ></div>
    <!-- 内容区,展示10条数据,注意其定位的top值是变化的 -->
    <div class="contentList" :style="{ top: topVal }">
      <!-- 每一条(项)数据 -->
      <div
        v-for="(item, index) in showListData"
        :key="index"
        class="itemClass"
        :style="{ height: itemHeight + 'px' }"
      >
        {{ item.name }}
      </div>
    </div>
    <!-- 加载中部分 -->
    <div class="loadingBox" v-show="loading">
      <i class="el-icon-loading"></i>
      &nbsp;&nbsp;<span>loading...</span>
    </div>
  </div>
</template>
<script>
import axios from "axios";
export default {
  data() {
    return {
      allListData: [], // 所有的数据,比如这个数组存放了十万条数据
      itemHeight: 40, // 每一条(项)的高度,比如40像素
      count: 10, // 一屏展示几条数据
      start: 0, // 开始位置的索引
      end: 10, // 结束位置的索引
      topVal: 0, // 父元素滚动条滚动,更改子元素对应top定位的值,确保联动
      loading: false,
    };
  },
  computed: {
    // 从所有的数据allListData中截取需要展示的数据showListData
    showListData: function () {
      return this.allListData.slice(this.start, this.end);
    },
  },
  async created() {
    this.loading = true;
    const res = await axios.get("http://ashuai.work:10000/bigData");
    this.allListData = res.data.data;
    this.loading = false;
  },
  methods: {
    // 滚动这里可以加上节流,减少触发频次
    handleScroll() {
      /**
       * 获取在垂直方向上,滚动条滚动了多少像素距离Element.scrollTop
       *
       * 滚动的距离除以每一项的高度,即为滚动到了多少项,当然,要取个整数
       * 例:滚动4米,一步长0.8米,滚动到第几步,4/0.8 = 第5步(取整好计算)
       *
       * 又因为我们一次要展示10项,所以知道了起始位置项,再加上结束位置项,
       * 就能得出区间了【起始位置, 起始位置 + size项数】==【起始位置, 结束位置】
       * */
      const scrollTop = this.$refs.virtualListWrap.scrollTop;
      this.start = Math.floor(scrollTop / this.itemHeight);
      this.end = this.start + this.count;
      /**
       * 动态更改定位的top值,确保联动,动态展示相应内容
       * */
      this.topVal = this.$refs.virtualListWrap.scrollTop + "px";
    },
  },
};
</script>
<style scoped lang="less">
// 虚拟列表容器盒子
.virtualListWrap {
  box-sizing: border-box;
  width: 240px;
  border: solid 1px #000000;
  // 开启滚动条
  overflow-y: auto;
  // 开启相对定位
  position: relative;
  .contentList {
    width: 100%;
    height: auto;
    // 搭配使用绝对定位
    position: absolute;
    top: 0;
    left: 0;
    .itemClass {
      box-sizing: border-box;
      width: 100%;
      height: 40px;
      line-height: 40px;
      text-align: center;
    }
    // 奇偶行改一个颜色
    .itemClass:nth-child(even) {
      background: #c7edcc;
    }
    .itemClass:nth-child(odd) {
      background: pink;
    }
  }
  .loadingBox {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(255, 255, 255, 0.64);
    color: green;
    display: flex;
    justify-content: center;
    align-items: center;
  }
}
</style>

使用vxetable插件实现虚拟列表

如果不是列表,是table表格的话,笔者这里推荐一个好用的UI组件,vxetable,看名字就知道做的是表格相关的业务。其中就包括虚拟列表。

vue2vue3版本都支持,性能比较好,官方说:虚拟滚动(最大可以支撑 5w 列、30w 行)

强大!

官方网站地址:vxetable.cn/v3/#/table/...

安装使用代码

注意安装版本,笔者使用的版本如下:

cnpm i xe-utils vxe-table@3.6.11 --save

main.js

js 复制代码
// 使用VXETable
import VXETable from 'vxe-table'
import 'vxe-table/lib/style.css'
Vue.use(VXETable)

具体代码如下:

html 复制代码
<template>
  <div class="box">
    <vxe-table
      border
      show-overflow
      ref="xTable1"
      height="300"
      :row-config="{ isHover: true }"
      :loading="loading"
    >
      <vxe-column type="seq"></vxe-column>
      <vxe-column field="id" title="ID"></vxe-column>
      <vxe-column field="name" title="名字"></vxe-column>
      <vxe-column field="value" title="对应值"></vxe-column>
    </vxe-table>
  </div>
</template>

<script>
import axios from "axios";
export default {
  data() {
    return {
      loading: false,
    };
  },
  async created() {
    this.loading = true;
    const res = await axios.get("http://ashuai.work:10000/bigData");
    this.loading = false;
    this.render(res.data.data);
  },
  methods: {
    render(data) {
      this.$nextTick(() => {
        const $table = this.$refs.xTable1;
        $table.loadData(data);
      });
    },
  },
};
</script>
相关推荐
丁总学Java7 分钟前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
It'sMyGo16 分钟前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀18 分钟前
CSS——属性值计算
前端·css
李是啥也不会32 分钟前
数组的概念
javascript
无咎.lsy43 分钟前
vue之vuex的使用及举例
前端·javascript·vue.js
fishmemory7sec1 小时前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron
fishmemory7sec1 小时前
Electron 使⽤ electron-builder 打包应用
前端·javascript·electron
豆豆2 小时前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
JUNAI_Strive_ving2 小时前
番茄小说逆向爬取
javascript·python
看到请催我学习2 小时前
如何实现两个标签页之间的通信
javascript·css·typescript·node.js·html5