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 相关文章推荐
浅析javascript闭包 实例分析
Dec 25 Javascript
JavaScript之自定义类型
May 04 Javascript
javascript模拟枚举的简单实例
Mar 06 Javascript
JS设置cookie、读取cookie
Feb 24 Javascript
Javascript实现代码折叠功能
Aug 25 Javascript
js实现鼠标左右移动,图片也跟着移动效果
Jan 25 Javascript
vue过渡和animate.css结合使用详解
Jun 14 Javascript
使用socket.io实现简单聊天室案例
Jan 02 Javascript
vue.js实现的绑定class操作示例
Jul 06 Javascript
深入浅出理解JavaScript闭包的功能与用法
Aug 01 Javascript
js HTML DOM EventListener功能与用法实例分析
Apr 27 Javascript
Vue select 绑定动态变量的实例讲解
Oct 22 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
Search Engine Friendly的URL设计
2006/10/09 PHP
PHP循环结构实例讲解
2014/02/10 PHP
PHP中把数据库查询结果输出为json格式简单实例
2015/04/09 PHP
php使用FFmpeg接口获取视频的播放时长、码率、缩略图以及创建时间
2016/11/07 PHP
Laravel如何实现自动加载类
2019/10/14 PHP
自制基于jQuery的智能提示插件一枚
2011/02/18 Javascript
javascript如何动态加载表格与动态添加表格行
2013/11/27 Javascript
js模拟淘宝网的多级选择菜单实现方法
2015/08/18 Javascript
详解AngularJS过滤器的使用
2016/03/11 Javascript
JavaScript学习笔记之数组的增、删、改、查
2016/03/23 Javascript
JS瀑布流实现方法实例分析
2016/12/19 Javascript
jQuery和CSS仿京东仿淘宝列表导航菜单
2017/01/04 Javascript
ionic中的$ionicPlatform.ready事件中的通用设置
2017/06/11 Javascript
Node.js使用orm2进行update操作时关联字段无法修改的解决方法
2017/06/13 Javascript
浅析Vue自定义组件的v-model
2017/11/26 Javascript
实例学习JavaScript读取和写入cookie
2018/01/29 Javascript
详解基于vue-cli3.0如何构建功能完善的前端架子
2018/10/09 Javascript
详解 微信小程序开发框架(MINA)
2019/05/17 Javascript
vue实现Excel文件的上传与下载功能的两种方式
2019/06/28 Javascript
微信小程序tabBar 返回tabBar不刷新页面
2019/07/25 Javascript
python删除列表中重复记录的方法
2015/04/28 Python
Python编写简单的HTML页面合并脚本
2016/07/11 Python
Python正则表达式经典入门教程
2017/05/22 Python
分析python动态规划的递归、非递归实现
2018/03/04 Python
使用Python和xlwt向Excel文件中写入中文的实例
2018/04/21 Python
Python 循环语句之 while,for语句详解
2018/04/23 Python
python统计字母、空格、数字等字符个数的实例
2018/06/29 Python
K最近邻算法(KNN)---sklearn+python实现方式
2020/02/24 Python
澳大利亚领先的在线机械五金、园艺和存储专家:Edisons
2018/03/24 全球购物
项目考察欢迎辞
2014/01/17 职场文书
农民致富事迹材料
2014/01/23 职场文书
个人工作主要事迹
2014/05/08 职场文书
土木工程求职信
2014/05/29 职场文书
2015年建筑工作总结报告
2015/05/04 职场文书
2015年妇幼保健工作总结
2015/05/19 职场文书
故意伤害罪辩护词
2015/05/21 职场文书