js前端对于大量数据的展示方式及处理方法


Posted in Javascript onDecember 02, 2020

最近暂时脱离了演示项目,开始了公司内比较常见的以表单和列表为主的项目。
干一个,爱一个了。从开始的觉得自己都做了炫酷的演示项目了,这对我来说就是个小意思,慢慢也开始踩坑,有了些经验总结可谈。

现下不得不说是个数据的时代,有数据就必定有前端来展示。
杂乱的数据通过数据分析(未碰到的点,不讲请搜),提炼出业务相关的数据维度,而前端所做的就是把这些一个个数据通过不同维度(key-value)的描述来展示到页面上。

除去花哨的展示方式(图表等),展示普通的大量列表数据有两种常用方式,分页和触底加载(滚动加载)。

分页是一种比较经典的展示方式,碰到的问题比较少,最多是因为一页展示的数据量大些的时候可以用图片懒加载,来加速一些(不过基本一页也不太会超过200个,不然就失去了分页的意义了)。

js前端对于大量数据的展示方式及处理方法

而最近在实现滚动加载时,出现了卡顿的情况。

js前端对于大量数据的展示方式及处理方法

问题背景:

数据量:1500左右;
数据描述形式:图片 + 部分文字描述;
卡顿出现在两个地方:

滚动卡顿,往往是动一下滚轮,就要卡个2-3s
单个数据卡片事件响应卡顿:鼠标浮动,本应0.5s向下延展,但是延展之前也会卡个1-2s;鼠标点击,本应弹出图片大图,但是弹出前也要卡个1-2s

js前端对于大量数据的展示方式及处理方法

分析过程:

卡顿首先想到是渲染帧被延长了,用控制台的Performance查看,可以看出是重排重绘费时间:

js前端对于大量数据的展示方式及处理方法

如图,Recalculate Style占比远远大于其他,一瞬间要渲染太多的卡片节点,重排重绘的量太大,所以造成了主要的卡顿。
因此,需要减少瞬间的渲染量。

渲染的数据项与图片渲染有关,于是会想到图片资源的加载和渲染,看控制台的Network的Img请求中,有大量的pending项(pending项参考下图所示)。

js前端对于大量数据的展示方式及处理方法

图片在不停地加载然后渲染,影响了页面的正常运行,因此可以作懒加载优化。

解决过程:

首先针对最主要的减少瞬间渲染量,逐步由简入繁尝试:

1. 自动触发的延时渲染
由定时器来操作,setTimeout和setInterval都可以,注意及时跳出循环即可。
我使用了setTimeout来作为第一次尝试(下面代码为后续补的手写,大概意思如此)

使用定时器来分页获取数据,然后push进展示的列表数据中:

data() {
 return {
  count: -1,
  params: {
   ... // 请求参数
   pageNo: 0,
   pageSize: 20
  },
  timer:null,
  list: []
 }
},
beforeDestroy() {
 if (this.timer) {
  clearTimeout(this.timer)
  this.timer = null
 }
},
methods: {
 getListData() {
  this.count = -1
  this.params = {
   ... // 请求参数
   pageNo: 0,
   pageSize: 20
  }
  this.timer = setTimeout(this.getListDataInterval, 1000)
 },
 getListDataInterval() {
  params.pageNo++
  if (params.pageNo === 1) {
   this.list.length = 0
  }
  api(params) // 请求接口
   .then(res => {
    if (res.data) {
     this.count = res.data.count
     this.list.push(...res.data.list)
    }
   })
   .finally(() => {
    if (count >= 0 && this.list.length < count) {
     this.timer = setTimeout(this.getListDataInterval, 1000)
    }
   })
 }
 ...
}

结果:首屏渲染速度变快了,不过滚动和事件响应还是略卡顿。
原因分析:滚动的时候还是有部分数据在渲染和加载,其次图片资源的加载渲染量未变(暂未作图片懒加载)。

2. 改为滚动触发加载(滚动触发下的“分页”形容的是数据分批次)

滚动触发,好处在于只会在触底的情况下影响用户一段时间,不会在开始时一直影响用户,而且触底也是由用户操作概率发生的,相对比下,体验性增加。
此处有两种做法:

滚动触发“分页”请求数据,
缺点:除了第一次,之后每次滚动触发展示数据会比下一种耗费多一个请求的时间
一次性获取所有数据存在内存中,滚动触发“分页”展示数据。
缺点:第一次一次性获取所有数据的时间,比上一种耗费多一点时间
上述两种做法,可视数据的具体数量决定(据同事所尝试,两三万个数据的获取时间在1s以上,不过这个也看数据结构的复杂程度和后端查数据的方式),决定前可以调后端接口试一下时间。

例:结合我本次项目的实际情况,不需要一次性获取所有的数据,可以一次性获取一个时间点的数据,而每个时间点的数据不会超过3600个,这就属于一个比较小的量,尝试下来一次性获取的时间基本不超过500ms,于是我选择第二种

先一次性获取所有数据,由前端控制滚动到距离底部的一定距离,push一定量的数据到展示列表数据中:

data() {
 return {
  timer: null,
  list: [], // 存储数据的列表
  showList: [], // html中展示的列表
  isLoading: false, // 控制滚动加载
  currentPage: 1, // 前端分批次摆放数据
  currentPageSize: 50, // 前端分批次摆放数据
  lastListIndex: 0, // 记录当前获取到的最新数据位置
  lastTimeIndex: 0, // 记录当前获取到的最新数据位置
 }
},
created() { // 优化点:可做可不做,其中的数值都是按照卡片的宽高直接写入的,因为不是通用组件,所以从简。
 this.currentPageSize = Math.round(
  (((window.innerHeight / 190) * (window.innerWidth - 278 - 254)) / 220) * 3
 ) // (((window.innerHeight / 卡片高度和竖向间距) * (window.innerWidth - 列表内容距视口左右的总距离 - 卡片宽度和横向间距)) / 卡片宽度) * 3
// *3代表我希望每次加载至少能多出三个视口高度的数据;列表内容距视口左右的总距离:是因为我是两边固定宽度,中间适应展示内容的结构
},
beforeDestroy() {
 if (this.timer) {
  clearTimeout(this.timer)
  this.timer = null
 }
},
methods: {
 /**
  * @description: 获取时间点的数据
  */
 getTimelineData(listIndex, timeIndex) {
  if (
   // this.list的第一、二层是时间轴this.list[listIdex].timeLines[timeIndex],在获取时间点数据之前获取了
   this.list &&
   this.list[listIndex] &&
   this.list[listIndex].timeLines &&
   this.list[listIndex].timeLines[timeIndex] &&
   this.showList &&
   this.showList[listIndex] &&
   this.showList[listIndex].timeLines &&
   this.showList[listIndex].timeLines[timeIndex]
  ) {
   this.isLoading = true
   // 把当前时间点变成展示状态
   if (!this.showList[listIndex].active) {
    this.handleTimeClick(listIndex, this.showList[listIndex])
   }
   if (!this.showList[listIndex].timeLines[timeIndex].active)
    this.handleTimeClick(
     listIndex,
     this.showList[listIndex].timeLines[timeIndex]
    )
   if (!this.list[listIndex].timeLines[timeIndex].snapDetailList) {
    this.currentPage = 1
   }
   if (
    !this.list[listIndex].timeLines[timeIndex].snapDetailList // 第一次加载时间点数据,后面的或条件可省略
   ) {
    
    return suspectSnapRecords({
     ...
    })
     .then(res => {
      if (res.data && res.data.list && res.data.list.length) {
       let show = []
       res.data.list.forEach((item, index) => {
        show[index] = {}
        if (index < 50) {
         show[index].show = true
        } else {
         show[index].show = true
        }
       })
       this.$set(
        this.list[listIndex].timeLines[timeIndex],
        'snapDetailList',
        res.data.list
       )
       this.$set(
        this.showList[listIndex].timeLines[timeIndex],
        'snapDetailList',
        res.data.list.slice(0, this.currentPageSize)
       )
       this.$set(
        this.showList[listIndex].timeLines[timeIndex],
        'showList',
        show
       )
       this.currentPage++
       this.lastListIndex = listIndex
       this.lastTimeIndex = timeIndex
      }
     })
     .finally(() => {
      this.$nextTick(() => {
       this.isLoading = false
      })
     })
   } else { // 此处是时间点被手动关闭,手动关闭会把showList中的数据清空,但是已经加载过数据的情况
    if (
     this.showList[listIndex].timeLines[timeIndex].snapDetailList
      .length === 0
    ) {
     this.currentPage = 1
     this.lastListIndex = listIndex
     this.lastTimeIndex = timeIndex
    }
    this.showList[listIndex].timeLines[timeIndex].snapDetailList.push(
     ...this.list[listIndex].timeLines[timeIndex].snapDetailList.slice(
      (this.currentPage - 1) * this.currentPageSize,
      this.currentPage * this.currentPageSize
     )
    )
    this.currentPage++
    this.$nextTick(() => {
     this.isLoading = false
    })
    return
   }
  } else {
   return
  }
 },
 /**
  * @description: 页面滚动监听,用的是公司内部的框架,就不展示html了,不同框架原理都是一样的,只是需要写的代码多与少的区别,如ElementUI的InfiniteScroll,可以直接设置触发加载的距离阈值
  */
 handleScroll({ scrollTop, percentY }) { // 此处的scrollTop是组件返回的纵向滚动的已滚动距离,percentY则是已滚动百分比
   this.bus.$emit('scroll') // 触发全局的滚动监听,用于图片的懒加载
   this.scrolling = true
   if (this.timer) { // 防抖机制,直至滚动停止才会运行定时器内部内容
    clearTimeout(this.timer)
   }
   this.timer = setTimeout(() => {
    requestAnimationFrame(async () => {
     // 因为内部有触发重排重绘,所以把代码放在requestAnimationFrame中执行
     let height = window.innerHeight
     if (
      percentY > 0.7 && // 保证最开始的时候不要疯狂加载,已滚动70%再加载
      Math.round(scrollTop / percentY) - scrollTop < height * 2 && // 保证数据量大后滚动页面长的时候不要疯狂加载,在触底小于两倍视口高度的时候才加载
      !this.isLoading // 保险,不同时运行下面代码,以防运行时间大于定时时间
     ) {
      this.isLoading = true
      let len = this.list[this.lastListIndex].timeLines[
       this.lastTimeIndex
      ].snapDetailList.length // list为一次性获取所有数据存在内存中
      if ((this.currentPage - 1) * this.currentPageSize < len) { // 前端分批次展示的情况
       this.showList[this.lastListIndex].timeLines[
        this.lastTimeIndex
       ].snapDetailList.push(
        ...this.list[this.lastListIndex].timeLines[
         this.lastTimeIndex
        ].snapDetailList.slice(
         (this.currentPage - 1) * this.currentPageSize,
         this.currentPage * this.currentPageSize
        )
       )
       this.currentPage++
      } else if (
       this.list[this.lastListIndex].timeLines.length >
       this.lastTimeIndex + 1
      ) { // 前端分批次展示完上一波数据,该月份时间轴上下一个时间点存在的情况
       await this.getTimelineData(
        this.lastListIndex,
        this.lastTimeIndex + 1
       )
      } else if (this.list.length > this.lastTimeIndex + 1) { // 前端分批次展示完上一波数据,该月份时间轴上下一个时间点不存在,下一个月份存在的情况
       await this.getTimelineData(this.lastListIndex + 1, 0)
      }
     }
     this.$nextTick(() => {
      this.isLoading = false
      this.scrolling = false
     })
    })
   }, 500)
  },

结果:首屏渲染和事件响应都变快了,只是滑动到底部的时候有些许卡顿。
原因分析:滑动到底部的卡顿,也是因为一瞬间渲染一堆数据,虽然比一次性展示所有的速度快很多,但是还是存在相比一次性展示不那么严重的重排和重绘,以及图片不停加载渲染的情况。

3. 滚动触发+图片懒加载

图片懒加载可以解决每次渲染数据的时候因为图片按加载顺序不停渲染产生的卡顿。
滚动触发使用点2的代码。
提取通用的图片组件,通过滚动事件的全局触发,来控制每个数据项图片的加载:
如上,点2中已经在handleScroll中设置了 this.bus.$emit('scroll') // 触发全局的滚动监听,用于图片的懒加载

// main.js
Vue.prototype.bus = new Vue()
...

以下的在template中写js不要学噢

// components/DefaultImage.vue
<template>
 <div class="default-image" ref="image">
  <img src="@/assets/images/image_empty.png" v-if="imageLoading" />
  <img
   class="image"
   v-if="showSrc"
   v-show="!imageLoading && !imageError"
   :src="showSrc"
   @load="imageLoading = false"
   @error="
    imageLoading = false
    imageError = true
   "
  />
  <img src="@/assets/images/image_error.png" v-if="imageError" />
 </div>
</template>
<script>
export default {
 name: 'DefaultImage',
 props: {
  src: String, // 图片源
  lazy: Boolean // 懒加载
 },
 data() {
  return {
   imageLoading: true,
   imageError: false,
   showSrc: '', // 渲染的src
   timer: null
  }
 },
 mounted() {
  if (this.lazy) {
   this.$nextTick(() => {
    this.isShowImage()
   })
   this.bus.$on('scroll', this.handleScroll)
  } else {
   this.showSrc = this.src
  }
 },
 beforeDestroy() {
  if (this.lazy) {
   this.bus.$off('scroll', this.handleScroll)
  }
  if (this.timer) {
   clearTimeout(this.timer)
   this.timer = null
  }
 },
 methods: {
  handleScroll() {
   if (this.timer) {
    clearTimeout(this.timer)
   }
   this.timer = setTimeout(this.isShowImage, 300)
  },
  isShowImage() {
   let image = this.$refs.image
   if (image) {
    let rect = image.getBoundingClientRect()
    const yInView = rect.top < window.innerHeight && rect.bottom > 0
    const xInView = rect.left < window.innerWidth && rect.right > 0
    if (yInView && xInView) {
     this.showSrc = this.src
     this.bus.$off('scroll', this.handleScroll)
    }
   }
  }
 }
}
</script>

结果:在点2首屏展示快的基础上,事件交互更快了,触发展示数据也快了。
原因分析:防抖的图片懒加载之后,只在用户滚动停止时,加载视口内的图片,就没有后续不断的加载渲染图片,也就不会因为不停渲染图片而影响事件交互和基础的无图卡片渲染。

以上一顿操作之后已经符合本项目的需求了。
不过我研究了一下进阶操作 ?
还可以只渲染视口元素,非视口用padding代替,以及把计算过程放在Web Worker多线程执行,进一步提升速度。
待我研究一下操作补上

以上就是js前端对于大量数据的展示方式及处理方法的详细内容,更多关于js 大量数据展示及处理的资料请关注三水点靠木其它相关文章!

Javascript 相关文章推荐
JScript中的&quot;this&quot;关键字使用方式补充材料
Mar 08 Javascript
JavaScript的setAttribute兼容性问题解决方法
Nov 11 Javascript
javascript获取URL参数与参数值的示例代码
Dec 20 Javascript
Bootstrap table 定制提示语的加载过程
Feb 20 Javascript
Angular.JS通过指令操作DOM的方法
May 10 Javascript
ztree实现权限横向显示功能
May 20 Javascript
js编写选项卡效果
May 23 Javascript
vue 动态改变静态图片以及请求网络图片的实现方法
Feb 07 Javascript
node实现基于token的身份验证
Apr 09 Javascript
JSON数据中存在单个转义字符“\”的处理方法
Jul 11 Javascript
解决vue中修改了数据但视图无法更新的情况
Aug 27 Javascript
在vant 中使用cell组件 定义图标该图片和位置操作
Nov 02 Javascript
vue3.0中setup使用(两种用法)
Dec 02 #Vue.js
JavaScript 如何在浏览器中使用摄像头
Dec 02 #Javascript
vue3.0+vue-router+element-plus初实践
Dec 02 #Vue.js
JavaScript实现简单动态表格
Dec 02 #Javascript
JavaScript实现10秒后再次获取验证码
Dec 02 #Javascript
JavaScript实现网页跨年倒计时
Dec 02 #Javascript
JavaScript async/await原理及实例解析
Dec 02 #Javascript
You might like
如何使用jQuery+PHP+MySQL来实现一个在线测试项目
2015/04/26 PHP
php curl 模拟登录并获取数据实例详解
2016/12/22 PHP
Laravel框架用户登陆身份验证实现方法详解
2017/09/14 PHP
PHP7匿名类的用法示例
2019/04/05 PHP
jquery 常用操作整理 基础入门篇
2009/10/14 Javascript
javascript 函数调用的对象和方法
2010/07/01 Javascript
6款新颖的jQuery和CSS3进度条插件推荐
2013/03/05 Javascript
JS实现标签页效果(配合css)
2013/04/03 Javascript
jquery实现图片左右间隔滚动特效(可自动播放)
2013/05/08 Javascript
基于ajax实现文件上传并显示进度条
2015/08/03 Javascript
jQuery动画显示和隐藏效果实例演示(附demo源码下载)
2015/12/31 Javascript
js中小数向上取整数,向下取整数,四舍五入取整数的实现(必看篇)
2017/02/13 Javascript
jqgrid实现简单的单行编辑功能
2017/09/30 Javascript
JS设计模式之命令模式概念与用法分析
2018/02/06 Javascript
超出JavaScript安全整数限制的数字计算BigInt详解
2018/06/24 Javascript
Vue.js 父子组件通信的十种方式
2018/10/30 Javascript
为什么要使用Vuex的介绍
2019/01/19 Javascript
浅谈JavaScript_DOM学习篇_图片切换小案例
2019/03/19 Javascript
在weex中愉快的使用scss的方法步骤
2020/01/02 Javascript
vue 动态添加class,三个以上的条件做判断方式
2020/11/02 Javascript
Python类的用法实例浅析
2015/05/27 Python
python 将大文件切分为多个小文件的实例
2019/01/14 Python
如何使用python3获取当前路径及os.path.dirname的使用
2019/12/13 Python
使用Python求解带约束的最优化问题详解
2020/02/11 Python
Pycharm中安装Pygal并使用Pygal模拟掷骰子(推荐)
2020/04/08 Python
Python爬取YY评级分数并保存数据实现过程解析
2020/06/01 Python
Tea Collection官网:一家位于旧金山的童装公司
2020/08/07 全球购物
毕业生精彩的自我评价分享
2013/10/06 职场文书
班级学雷锋活动总结
2014/06/26 职场文书
金融管理专业求职信
2014/07/10 职场文书
小学生一分钟演讲稿
2014/08/26 职场文书
环卫工作汇报材料
2014/10/28 职场文书
合作意向协议书
2015/01/29 职场文书
解析在浏览器地址栏输入一个URL后发生了什么
2021/06/21 Servers
基于Redis的List实现特价商品列表功能
2021/08/30 Redis
Pandas 数据编码的十种方法
2022/04/20 Python