关于性能优化:什么后端要一次性返回我10万条数据且看我这8种方案机智应对

39次阅读

共计 11281 个字符,预计需要花费 29 分钟才能阅读完成。

问题形容

  • 面试官:后端一次性返回 10 万条 数据给你,你如何解决?
  • 我:歪嘴一笑,what the f**k!

问题考察点

看似无厘头的问题,实际上考查候选人 常识的广度和深度,尽管在工作中这种状况很少遇到 …

  • 考查前端如何解决大量数据
  • 考查候选人对于大量数据的性能优化
  • 考查候选人解决问题的思考形式(对于这一点,文末会说到,大家持续浏览)
  • ……

文末会提供残缺代码,供大家更好的了解

应用 express 创立一个十万条数据的接口

若是道友对 express 相干不太熟悉的话,有空能够看看笔者的这一篇全栈文章(还有残缺代码哦):《Vue+Express+Mysql 全栈我的项目之增删改查、分页排序导出表格性能》

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}) // 将十万条数据返回之
})

点击按钮,发申请,获取数据,渲染到表格上

html 构造如下:

<el-button :loading="loading" @click="plan"> 点击申请加载 </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 plan() {// 发申请,拿数据,赋值给 arr}

计划一 间接渲染所有数据

如果申请到 10 万条数据间接渲染,页面会卡死的,很显然,这种形式是不可取的

 async plan() {
      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 条数据,那么十万条数据就有一万堆
  • 应用定时器,一次渲染一堆,渲染一万次即可
  • 这样做的话,页面就不会卡死了

用户所看到的效果图是如下

效果图

分组分批分堆函数

  • 咱们先写一个函数,用于将 10 万条数据进行分堆
  • 所谓的分堆其实 思维就是一次截取肯定长度的数据
  • 比方一次截取 10 条数据,头一次截取 0~9,第二次截取 10~19 等固定长度的截取
  • 举例原来的数据是:[1,2,3,4,5,6,7]
  • 假如咱们分堆当前,一堆分 3 个,那么失去的后果就是二维数组了
  • 即:[[1,2,3], [4,5,6], [7]]
  • 而后就遍历这个二维数组,失去每一项的数据,即为每一堆的数据
  • 进而应用定时器一点点、一堆堆赋值渲染即可

分组分批分堆函数(一堆分 10 个)

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. 最初把后果丢进来即可
}

创立定时器去顺次赋值渲染

比方咱们每隔一秒钟去赋值渲染一次

  async plan() {
      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定时器 长处,道友们能够看笔者的这篇文章:《性能优化之通俗易懂学习 requestAnimationFrame 和应用场景举例》

反正大家遇到定时器的时候,就能够考虑一下,是否能够应用申请动画帧进行优化执行渲染?

如果应用申请动画帧的话,就要批改一下代码写法了,后面的不变动,plan 办法中的写法变一下即可,留神正文:

async plan() {
  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); 
},

计划四 搭配分页组件,前端进行分页(每页展现一堆,分堆思维)

这种形式,笔者已经遇到过,过后的对应场景是数据量也就几十条,后端间接把几十条数据丢给前端,让前端去分页

后端不做分页的起因是。他过后长期有事件销假了,所以就前端去做分页了。

  • 数据量大的状况下,这种形式,也是一种解决方案
  • 思路也是在所有数据的根底上进行截取
  • 简要代码如下:
getShowTableData() { 
    // 获取截取开始索引 
    let begin = (this.pageIndex - 1) * this.pageSize; 
    // 获取截取完结索引
     let end = this.pageIndex * this.pageSize; 
    // 通过索引去截取,从而展现
    this.showTableData = this.allTableData.slice(begin, end); 
}

残缺案例代码,请看笔者的这篇文章:《后端一次性返回所有的数据,让前端截取展现做分页》

实际上,这种大工作拆分成许多小工作,这种形式,做法,利用的思维就是分片的形式(工夫),在别的场景,比方大文件上传的时候,也有这种思维,比方一个 500MB 的大文件,拆分成 50 个小文件,一个是 10MB 这样... 至于大文件上传的文章,那就等笔者有空了再写呗...

计划五 表格滚动触底加载(滚动到底,再加载一堆)

这里重点就是咱们须要去判断,何时滚动条触底。判断形式次要有两种

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

目前市面上支流的一些插件的原理,大抵是这两种。

笔者举例的这是,是应用的插件 v-el-table-infinite-scroll,实质上这个插件是一个自定义指令。对应npm 地址:https://www.npmjs.com/package…

当然也有别的插件,如 vue-scroller 等:一个意思,不赘述

留神,触底加载也是要分堆的,将发申请获取到的十万条数据,进行分好堆,而后每触底一次,就加载一堆即可

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

装置,留神版本号(辨别 vue2 和 vue3)

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

注册应用指令插件

// 应用有限滚动插件
import elTableInfiniteScroll from 'el-table-infinite-scroll';
Vue.use(elTableInfiniteScroll);

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

<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() {// 触底加载,展现数据...},

案例代码

为了不便大家演示,这里笔者间接附上一个案例代码,留神看其中的 步骤 正文即可

<template>
  <div class="box">
    <el-table
      v-el-table-infinite-scroll="load"
      height="600"
      :data="tableData"
      border
      style="width: 80%"
      v-loading="loading"
      element-loading-text="数据量太大啦,客官稍后..."
      element-loading-spinner="el-icon-loading"
      element-loading-background="rgba(255, 255, 255, 0.5)"
      :header-cell-style="{
        height: '24px',
        lineHeight: '24px',
        color: '#606266',
        background: '#F5F5F5',
        fontWeight: 'bold',
      }"
    >
      <el-table-column type="index" label="序"></el-table-column>
      <el-table-column prop="id" label="ID"></el-table-column>
      <el-table-column prop="name" label="名字"></el-table-column>
      <el-table-column prop="value" label="对应值"></el-table-column>
    </el-table>
  </div>
</template>

<script>
// 分堆函数
function averageFn(arr) {
  let i = 0;
  let result = [];
  while (i < arr.length) {result.push(arr.slice(i, i + 10)); // 一次截取 10 个用于分堆
    i = i + 10; // 这 10 个截取完,再筹备截取下 10 个
  }
  return result;
}
import axios from "axios";
export default {data() {
    return {allTableData: [], // 初始发申请获取所有的数据
      tableData: [], // 要展现的数据
      loading: false
    };
  },
  // 第一步,发申请,获取大量数据,并转成二维数组,分堆分组分块存储
  async created() {
    this.loading = true;
    const res = await axios.get("http://ashuai.work:10000/bigData");
    this.allTableData = averageFn(res.data.data); // 应用分堆函数,寄存二维数组
    // this.originalAllTableData = this.allTableData // 也能够存一份原始值,留作备用,都行的
    this.loading = false;
    // 第二步,操作结束当前,执行触底加载办法
    this.load();},
  methods: {
    // 初始会执行一次,当然也能够配置,使其不执行
    async load() {console.log("主动屡次执行之,首次执行会依据高度去计算要执行几次适合");
      // 第五步,触底加载相当于把二维数组的每一项取出来用,取完用完时 return 进行即可
      if (this.allTableData.length == 0) {console.log("没数据啦");
        return;
      }
      // 第三步,加载的时候,把二维数组的第一项取出来,拼接到要展现的表格数据中去
      let arr = this.allTableData[0];
      this.tableData = this.tableData.concat(arr);
      // 第四步,拼接展现当前,再把二维数组的第一项的数据删除即可
      this.allTableData.shift();},
  },
};
</script>

效果图

计划六 应用有限加载 / 虚构列表进行展现

什么是虚构列表?

  • 所谓的虚构列表实际上是 前端障眼法 的一种表现形式。
  • 看到的如同所有的数据都渲染了,实际上只渲染 可视区域 的局部罢了
  • 有点像咱们看电影,咱们看的话,是在一块电影屏幕上,一秒一秒的看(不停的放映)
  • 然而实际上电影有俩小时,如果把两个小时的电影都铺开的话,那得须要多少块电影屏幕呢?
  • 同理,如果 10 万条数据都渲染,那得须要多少 dom 节点元素呢?
  • 所以咱们只给用户看,他 当下能看到的
  • 如果用户要快进或快退(下拉滚动条或者上拉滚动条)
  • 再把对应的内容出现在电影屏幕上(出现在可视区域内)
  • 这样就实现了看着像是所有的 dom 元素每一条数据都有渲染的障眼法成果了

对于前端障眼法,在具体工作中,如果可能奇妙应用,会大大晋升咱们的开发效率的

写一个简略的虚构列表

效果图

这里笔者间接上代码,大家复制粘贴即可应用,笔者写了一些正文,以便于大家了解。当然也能够去笔者的仓库中去瞅瞅哦,GitHub 仓库在文末

代码

<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 行)

弱小!

官方网站地址:https://vxetable.cn/v3/#/tabl…

效果图

成果很丝滑

装置应用代码

留神装置版本,笔者应用的版本如下:

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

main.js

// 应用 VXETable
import VXETable from 'vxe-table'
import 'vxe-table/lib/style.css'
Vue.use(VXETable)

代码方面也很简略,如下:

<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>

计划七 开启多线程 Web Worker 进行操作

本案例中,应用 Web Worker 另外开启一个线程去操作代码逻辑,收益并不是特地大(如果应用虚构滚动列表插件的状况下)

不过也算是一个拓展的思路吧,面试的时候,倒是能够说一说,提一提。

Web Worker 不相熟的道友们,能够看看笔者之前的这篇文章:《性能优化之应用 vue-worker 插件(基于 Web Worker)开启多线程运算提高效率》

计划八 防患未然,防患于未然

以下为笔者愚见,仅供参考 …

  • 在上述解决方案都说完当前,并没有完结。
  • 实际上本题目在考查候选人常识的广度和深度以外,更是考查了候选人的解决问题的思考形式,这一点尤其重要!
  • 笔者曾做过候选人去求职,也曾做过面试官去面试。就程序员开发工作而言,技术知识点不相熟,能够疾速学习,如文档、谷歌、百度、技术交换群,相干共事都可提供肯定的反对
  • 更重要的是看中候选人的思考形式,思维模式
  • 试想,两个候选人实力程度差不多,然而一个只晓得埋头苦干,有活就干,不去斟酌;而另外一个却是在用心工作的时候,也会俯视星空,会剖析如何干活可能高性价比地实现工作,重视过程与后果
  • 这样的话,哪个更加受欢迎一些呢?

如果笔者是候选人,笔者在说了上述 7 种计划当前,会再补充第八种计划:防患未然,防患于未然


场景模仿

面试官随便端详着其手中我的简历,抚须怪叫一声:“小子,后端要一次性返回 10 万条数据给你,你如何解决?”

我眉毛一挑,歪嘴一笑:“在上述 7 种计划陈说完当前,我想相似的问题,咱们能够从根本上去解决。即第八种计划,要防患未然,防患于未然。”

“哦?”面试官心中纳闷,缓缓放下我的简历:“愿闻其详。”

我不紧不慢地答道:“在具体开发工作中,咱们在接到一个需要时,在技术评审期间,咱们就要和后端去磋商比拟适合的技术解决方案。这个问题是 后端要一次性返回我 10 万条数据,重点并不在 10 万条这么多数据,而在于后端为什么要这样做?”

面试官低头,认真听了起来。

我一字一顿地说道:“除去业务真正须要这种计划的话,后端这样做的起因大抵有两种,第一种他不太懂 sql 的 limit 语句,但这根本不可能,第二种就是他有事件,轻易搪塞写了一下。所以,就是要和他沟通,从大数据量接口申请时长过长,以及过多的 dom 元素渲染导致性能变差,以及我的项目的可维护性等角度去沟通,我置信只有正确的沟通,就能从本源下来防止这种不太正当的状况产生。”

面试官又忽然狡黠地提问:“要是沟通当前,后端死活不给你分页呢?你咋办?你的沟通无成果!你如何解决!人家不听你的!”仿佛是感觉这个问题很刁钻,他双臂抱在胸前,靠在椅背上,期待着我脸上行将绽开的的答复不上来地难堪笑容。

我心田冷哼一声:神通广大 …

我盯着面试官的眼睛,认真说道:“如果工作中沟通无成果,要么是我本人沟通语言表达的问题,这一点我会留神,一直晋升本人的沟通技巧和谈话形式,要么就是 …”

我声音扬起了三分:“我沟通的这个人有问题!他工作摸鱼偷懒耍滑!回心转意!尴尬别人!居高临下!自以为是!这种状况下,我会找到我的直属领导去染指,因为这曾经不是我的项目的需要问题了,而是员工的根本素养问题!”

进展了一秒,我声音又柔和了几分:“然而,然而我置信咱们公司员工中是相对没有这样的人存在的,各个都是能力强悍,态度端正的优秀员工。毕竟咱们公司在行业中久负盛名,我也是因而慕名而来的。您说对吧?”

面试官眼中闪过震惊之色,他没有想到我竟然把皮球又踢给他了,不过他为了维持形象,旋即复原了镇定,只是面部肌肉在止不住的微微颤动。

我又补充道:“实际上在工作中,前端作为比拟贴近用户的角色而言,须要和各个岗位的共事进行沟通,比方后端、产品、UI、测试等。咱们须要通过正当的沟通形式,去晋升工作效率,实现我的项目,实现本人的价值,为公司发明收益,我想这是每一个员工须要做的,也是必须要做到的。”

面试官又抚须怪叫一声:“小子体现还行,你被录用了!一个月工资 2200,自带电脑,无社无金,007 工作制,不能偷吃公司零食,以及 …”

我:阿哒,豪油跟,兰银缠绕,姬霓太美 …

总结

无效的沟通,源自于解决问题的思维模式,在少数状况下,重要性,大于当下所把握的技术知识点

  • 网站成果演示地址:http://ashuai.work:8888/#/big…
  • GitHub 仓库地址:https://github.com/shuirongsh…

如果感觉文章帮到了您,欢送不吝 star 哦 ^_^

正文完
 0