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 URL参数读取改进版
Jan 16 Javascript
使用jQuery实现验证上传图片的格式与大小
Dec 03 Javascript
jquery判断iPhone、Android设备类型
Sep 14 Javascript
Bootstrap jquery.twbsPagination.js动态页码分页实例代码
Feb 20 Javascript
Ionic2开发环境搭建教程
Aug 20 Javascript
BetterScroll 在移动端滚动场景的应用
Sep 18 Javascript
微信小程序分享功能之按钮button 边框隐藏和点击隐藏
Jun 14 Javascript
jQuery访问json文件中数据的方法示例
Jan 28 jQuery
vue.js+ElementUI实现进度条提示密码强度效果
Jan 18 Javascript
基于vue3.0.1beta搭建仿京东的电商H5项目
May 06 Javascript
javascript实现贪吃蛇游戏(娱乐版)
Aug 17 Javascript
JavaScript实现跟随鼠标移动的盒子
Jan 28 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与已存在的Java应用程序集成
2006/10/09 PHP
PHP中ob_start函数的使用说明
2013/11/11 PHP
浅谈PHP中pack、unpack的详细用法
2018/03/12 PHP
JqueryMobile动态生成listView并实现刷新的两种方法
2014/03/05 Javascript
JavaScript代码应该放在HTML代码哪个位置比较好?
2014/10/16 Javascript
跟我学习javascript的this关键字
2020/05/28 Javascript
jquery显示隐藏元素的实现代码
2016/05/19 Javascript
Bootstrap基本组件学习笔记之导航(10)
2016/12/07 Javascript
使用JavaScript触发过渡效果的方法
2017/01/19 Javascript
bootstrap响应式表格实例详解
2017/05/15 Javascript
JS实现把一个页面层数据传递到另一个页面的两种方式
2018/08/13 Javascript
js实现简单分页导航栏效果
2019/06/28 Javascript
微信小程序开发之map地图组件定位并手动修改位置偏差
2019/08/17 Javascript
微信小程序 冒泡事件原理解析
2019/09/27 Javascript
JavaScript相等运算符的九条规则示例详解
2019/10/20 Javascript
mpvue实现微信小程序快递单号查询代码
2020/04/03 Javascript
Vue-resource安装过程及使用方法解析
2020/07/21 Javascript
详解三种方式在React中解决绑定this的作用域问题并传参
2020/08/18 Javascript
如何利用vue实现波谱拟合详解
2020/11/05 Javascript
[01:07:15]DOTA2-DPC中国联赛 正赛 DLG vs XG BO3 第二场 1月25日
2021/03/11 DOTA
盘点提高 Python 代码效率的方法
2014/07/03 Python
python使用post提交数据到远程url的方法
2015/04/29 Python
Python画图学习入门教程
2016/07/01 Python
python中bs4.BeautifulSoup的基本用法
2019/07/27 Python
详解如何在cmd命令窗口中搭建简单的python开发环境
2019/08/29 Python
Python3.8对可迭代解包的改进及用法详解
2019/10/15 Python
python 用Matplotlib作图中有多个Y轴
2020/11/28 Python
HTML5 canvas实现雪花飘落特效
2016/03/08 HTML / CSS
Surfdome西班牙:世界上最受欢迎的生活方式品牌
2019/02/13 全球购物
双立人美国官方商店:ZWILLING集团餐具和炊具
2020/05/07 全球购物
师范学院毕业生求职信范文
2013/12/26 职场文书
科长个人四风问题整改措施思想汇报
2014/10/13 职场文书
2015年党性分析材料
2014/12/19 职场文书
作文评语怎么写
2014/12/25 职场文书
Windows server 2012 配置Telnet以及用法详解
2022/04/28 Servers
Hive HQL支持2种查询语句风格
2022/06/25 数据库