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 相关文章推荐
js跨域问题之跨域iframe自适应大小实现代码
Jul 17 Javascript
javascript中的float运算精度实例分析
Aug 21 Javascript
JavaScript实现的一个日期格式化函数分享
Dec 06 Javascript
angularJS提交表单(form)
Feb 09 Javascript
jQuery防止重复绑定事件的解决方法
May 14 Javascript
AngularJS 支付倒计时功能实现思路
Jun 05 Javascript
微信小程序“摇一摇”的实例代码
Jul 20 Javascript
JavaScript选取(picking)和反选(rejecting)对象的属性方法
Aug 16 Javascript
微信小程序radio组件使用详解
Jan 31 Javascript
判断iOS、Android以及PC端的示例代码
Nov 15 Javascript
Vue CLI4 Vue.config.js标准配置(最全注释)
Jun 05 Javascript
在HTML中使用JavaScript的两种方法
Dec 24 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 5.3.5安装memcache注意事项小结
2011/04/12 PHP
ThinkPHP中redirect用法分析
2014/12/05 PHP
php实现以只读方式打开文件的方法
2015/03/16 PHP
php中关于socket的系列函数总结
2015/05/18 PHP
实例分析PHP中PHPMailer发邮件
2017/12/13 PHP
laravel使用Faker数据填充的实现方法
2019/04/12 PHP
JavaScript 语言的递归编程
2010/05/18 Javascript
悬浮数字的实现案例
2014/02/19 Javascript
node.js中的fs.chmod方法使用说明
2014/12/18 Javascript
javascript实现左右控制无缝滚动
2014/12/31 Javascript
js实现完美兼容各大浏览器的人民币大小写相互转换
2015/10/29 Javascript
nodejs个人博客开发第四步 数据模型
2017/04/12 NodeJs
node.js基础知识小结
2018/02/26 Javascript
总结JavaScript在IE9之前版本中内存泄露问题
2018/04/28 Javascript
5分钟教你用nodeJS手写一个mock数据服务器的方法
2019/09/10 NodeJs
微信小程序网络请求实现过程解析
2019/11/06 Javascript
vue 组件销毁并重置的实现
2020/01/13 Javascript
工作中常用js功能汇总
2020/11/07 Javascript
python Django连接MySQL数据库做增删改查
2013/11/07 Python
python对csv文件追加写入列的方法
2019/08/01 Python
python实现复制文件到指定目录
2019/10/16 Python
Python 实现opencv所使用的图片格式与 base64 转换
2020/01/09 Python
让IE可以变相支持CSS3选择器
2010/01/21 HTML / CSS
斯德哥尔摩通票:Stockholm Pass
2018/01/09 全球购物
Gtech官方网站:地毯清洁器、吸尘器及园艺设备
2018/05/23 全球购物
FLOS美国官网:意大利高级照明工艺的传奇
2018/08/07 全球购物
公共汽车、火车和飞机票的通用在线预订和销售平台:INFOBUS
2019/11/30 全球购物
会计实习生自我鉴定
2013/12/12 职场文书
电大毕业自我鉴定
2014/02/03 职场文书
法人委托书的范本格式
2014/09/11 职场文书
党员个人对照检查材料范文
2014/09/24 职场文书
2015年教务工作总结
2015/05/23 职场文书
2016重阳节红领巾广播稿
2015/12/18 职场文书
商业计划书格式、范文
2019/03/21 职场文书
Java基础之this关键字的使用
2021/06/30 Java/Android
nginx sticky实现基于cookie负载均衡示例详解
2022/12/24 Servers