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 相关文章推荐
一个可以兼容IE FF的加为首页与加入收藏实现代码
Nov 02 Javascript
滚动条变色 隐藏滚动条与双击网页自动滚屏显示代码
Dec 28 Javascript
js 键盘记录实现(兼容FireFox和IE)
Feb 07 Javascript
JQUERY1.6 使用方法四 检测浏览器
Nov 23 Javascript
浏览器打开层自动缓慢展开收缩实例代码
Jul 04 Javascript
jquery解决客户端跨域访问问题
Jan 06 Javascript
Bootstrap CDN和本地化环境搭建
Oct 26 Javascript
jQuery实现浏览器之间跳转并传递参数功能【支持中文字符】
Mar 28 jQuery
ionic3双击返回退出应用的方法
Sep 17 Javascript
JS数组Reduce方法功能与用法实例详解
Apr 29 Javascript
详解react组件通讯方式(多种)
May 06 Javascript
uniapp开发小程序实现滑动页面控制元素的显示和隐藏效果
Dec 10 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
php遍历文件夹下的所有文件和子文件夹示例
2014/03/20 PHP
Smarty中常用变量操作符汇总
2014/10/27 PHP
PHP中的使用curl发送请求(GET请求和POST请求)
2017/02/08 PHP
js 编写规范
2010/03/03 Javascript
JavaScript字符串String和Array操作的有趣方法
2012/12/18 Javascript
Javascript Ajax异步读取RSS文档具体实现
2013/12/12 Javascript
jQuery队列操作方法实例
2014/06/11 Javascript
使用jQuery在移动页面上添加按钮和给按钮添加图标
2015/12/04 Javascript
jquery validate表单验证的基本用法入门
2016/01/18 Javascript
浅谈vue的几种绑定变量的值 防止其改变的方法
2018/03/01 Javascript
js+canvas绘制图形验证码
2020/09/21 Javascript
解决ant Design中Select设置initialValue时的大坑
2020/10/29 Javascript
数据挖掘之Apriori算法详解和Python实现代码分享
2014/11/07 Python
TensorFlow在MAC环境下的安装及环境搭建
2017/11/14 Python
PyTorch上搭建简单神经网络实现回归和分类的示例
2018/04/28 Python
python输出决策树图形的例子
2019/08/09 Python
pytorch 彩色图像转灰度图像实例
2020/01/13 Python
Pandas时间序列:时期(period)及其算术运算详解
2020/02/25 Python
Python selenium使用autoIT上传附件过程详解
2020/05/26 Python
Python通过zookeeper实现分布式服务代码解析
2020/07/22 Python
python 多线程共享全局变量的优劣
2020/09/24 Python
两种CSS3伪类选择器详细介绍
2013/12/24 HTML / CSS
基于HTML5代码实现折叠菜单附源码下载
2015/11/27 HTML / CSS
HTML5本地存储之IndexedDB
2017/06/16 HTML / CSS
瑞典领先的汽车零部件网上零售商:bildelaronline24.se
2017/01/12 全球购物
韩国美国时尚服装和美容在线全球市场:KOODING
2018/11/07 全球购物
char型变量中能不能存贮一个中文汉字
2015/07/08 面试题
企业晚会策划方案
2014/05/29 职场文书
金融与证券专业求职信
2014/06/22 职场文书
学习型党组织心得体会
2014/09/12 职场文书
2014年幼儿园班级工作总结
2014/12/17 职场文书
学习保证书怎么写
2015/02/26 职场文书
2019年大学生职业生涯规划书
2019/03/25 职场文书
python实现过滤敏感词
2021/05/08 Python
如何在Python项目中引入日志
2021/05/31 Python
vue项目如何打包之项目打包优化(让打包的js文件变小)
2022/04/30 Vue.js