js前端對于大量數(shù)據(jù)的展示方式及處理方法
最近暫時(shí)脫離了演示項(xiàng)目,開始了公司內(nèi)比較常見的以表單和列表為主的項(xiàng)目。
干一個(gè),愛一個(gè)了。從開始的覺得自己都做了炫酷的演示項(xiàng)目了,這對我來說就是個(gè)小意思,慢慢也開始踩坑,有了些經(jīng)驗(yàn)總結(jié)可談。
現(xiàn)下不得不說是個(gè)數(shù)據(jù)的時(shí)代,有數(shù)據(jù)就必定有前端來展示。
雜亂的數(shù)據(jù)通過數(shù)據(jù)分析(未碰到的點(diǎn),不講請搜),提煉出業(yè)務(wù)相關(guān)的數(shù)據(jù)維度,而前端所做的就是把這些一個(gè)個(gè)數(shù)據(jù)通過不同維度(key-value)的描述來展示到頁面上。
除去花哨的展示方式(圖表等),展示普通的大量列表數(shù)據(jù)有兩種常用方式,分頁和觸底加載(滾動(dòng)加載)。
分頁是一種比較經(jīng)典的展示方式,碰到的問題比較少,最多是因?yàn)橐豁撜故镜臄?shù)據(jù)量大些的時(shí)候可以用圖片懶加載,來加速一些(不過基本一頁也不太會(huì)超過200個(gè),不然就失去了分頁的意義了)。

而最近在實(shí)現(xiàn)滾動(dòng)加載時(shí),出現(xiàn)了卡頓的情況。

問題背景:
數(shù)據(jù)量:1500左右;
數(shù)據(jù)描述形式:圖片 + 部分文字描述;
卡頓出現(xiàn)在兩個(gè)地方:
滾動(dòng)卡頓,往往是動(dòng)一下滾輪,就要卡個(gè)2-3s
單個(gè)數(shù)據(jù)卡片事件響應(yīng)卡頓:鼠標(biāo)浮動(dòng),本應(yīng)0.5s向下延展,但是延展之前也會(huì)卡個(gè)1-2s;鼠標(biāo)點(diǎn)擊,本應(yīng)彈出圖片大圖,但是彈出前也要卡個(gè)1-2s

分析過程:
卡頓首先想到是渲染幀被延長了,用控制臺(tái)的Performance查看,可以看出是重排重繪費(fèi)時(shí)間:

如圖,Recalculate Style占比遠(yuǎn)遠(yuǎn)大于其他,一瞬間要渲染太多的卡片節(jié)點(diǎn),重排重繪的量太大,所以造成了主要的卡頓。
因此,需要減少瞬間的渲染量。
渲染的數(shù)據(jù)項(xiàng)與圖片渲染有關(guān),于是會(huì)想到圖片資源的加載和渲染,看控制臺(tái)的Network的Img請求中,有大量的pending項(xiàng)(pending項(xiàng)參考下圖所示)。

圖片在不停地加載然后渲染,影響了頁面的正常運(yùn)行,因此可以作懶加載優(yōu)化。
解決過程:
首先針對最主要的減少瞬間渲染量,逐步由簡入繁嘗試:
1. 自動(dòng)觸發(fā)的延時(shí)渲染
由定時(shí)器來操作,setTimeout和setInterval都可以,注意及時(shí)跳出循環(huán)即可。
我使用了setTimeout來作為第一次嘗試(下面代碼為后續(xù)補(bǔ)的手寫,大概意思如此)
使用定時(shí)器來分頁獲取數(shù)據(jù),然后push進(jìn)展示的列表數(shù)據(jù)中:
data() {
return {
count: -1,
params: {
... // 請求參數(shù)
pageNo: 0,
pageSize: 20
},
timer:null,
list: []
}
},
beforeDestroy() {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
},
methods: {
getListData() {
this.count = -1
this.params = {
... // 請求參數(shù)
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)
}
})
}
...
}
結(jié)果:首屏渲染速度變快了,不過滾動(dòng)和事件響應(yīng)還是略卡頓。
原因分析:滾動(dòng)的時(shí)候還是有部分?jǐn)?shù)據(jù)在渲染和加載,其次圖片資源的加載渲染量未變(暫未作圖片懶加載)。
2. 改為滾動(dòng)觸發(fā)加載(滾動(dòng)觸發(fā)下的“分頁”形容的是數(shù)據(jù)分批次)
滾動(dòng)觸發(fā),好處在于只會(huì)在觸底的情況下影響用戶一段時(shí)間,不會(huì)在開始時(shí)一直影響用戶,而且觸底也是由用戶操作概率發(fā)生的,相對比下,體驗(yàn)性增加。
此處有兩種做法:
滾動(dòng)觸發(fā)“分頁”請求數(shù)據(jù),
缺點(diǎn):除了第一次,之后每次滾動(dòng)觸發(fā)展示數(shù)據(jù)會(huì)比下一種耗費(fèi)多一個(gè)請求的時(shí)間
一次性獲取所有數(shù)據(jù)存在內(nèi)存中,滾動(dòng)觸發(fā)“分頁”展示數(shù)據(jù)。
缺點(diǎn):第一次一次性獲取所有數(shù)據(jù)的時(shí)間,比上一種耗費(fèi)多一點(diǎn)時(shí)間
上述兩種做法,可視數(shù)據(jù)的具體數(shù)量決定(據(jù)同事所嘗試,兩三萬個(gè)數(shù)據(jù)的獲取時(shí)間在1s以上,不過這個(gè)也看數(shù)據(jù)結(jié)構(gòu)的復(fù)雜程度和后端查數(shù)據(jù)的方式),決定前可以調(diào)后端接口試一下時(shí)間。
例:結(jié)合我本次項(xiàng)目的實(shí)際情況,不需要一次性獲取所有的數(shù)據(jù),可以一次性獲取一個(gè)時(shí)間點(diǎn)的數(shù)據(jù),而每個(gè)時(shí)間點(diǎn)的數(shù)據(jù)不會(huì)超過3600個(gè),這就屬于一個(gè)比較小的量,嘗試下來一次性獲取的時(shí)間基本不超過500ms,于是我選擇第二種
先一次性獲取所有數(shù)據(jù),由前端控制滾動(dòng)到距離底部的一定距離,push一定量的數(shù)據(jù)到展示列表數(shù)據(jù)中:
data() {
return {
timer: null,
list: [], // 存儲(chǔ)數(shù)據(jù)的列表
showList: [], // html中展示的列表
isLoading: false, // 控制滾動(dòng)加載
currentPage: 1, // 前端分批次擺放數(shù)據(jù)
currentPageSize: 50, // 前端分批次擺放數(shù)據(jù)
lastListIndex: 0, // 記錄當(dāng)前獲取到的最新數(shù)據(jù)位置
lastTimeIndex: 0, // 記錄當(dāng)前獲取到的最新數(shù)據(jù)位置
}
},
created() { // 優(yōu)化點(diǎn):可做可不做,其中的數(shù)值都是按照卡片的寬高直接寫入的,因?yàn)椴皇峭ㄓ媒M件,所以從簡。
this.currentPageSize = Math.round(
(((window.innerHeight / 190) * (window.innerWidth - 278 - 254)) / 220) * 3
) // (((window.innerHeight / 卡片高度和豎向間距) * (window.innerWidth - 列表內(nèi)容距視口左右的總距離 - 卡片寬度和橫向間距)) / 卡片寬度) * 3
// *3代表我希望每次加載至少能多出三個(gè)視口高度的數(shù)據(jù);列表內(nèi)容距視口左右的總距離:是因?yàn)槲沂莾蛇吂潭▽挾?,中間適應(yīng)展示內(nèi)容的結(jié)構(gòu)
},
beforeDestroy() {
if (this.timer) {
clearTimeout(this.timer)
this.timer = null
}
},
methods: {
/**
* @description: 獲取時(shí)間點(diǎn)的數(shù)據(jù)
*/
getTimelineData(listIndex, timeIndex) {
if (
// this.list的第一、二層是時(shí)間軸this.list[listIdex].timeLines[timeIndex],在獲取時(shí)間點(diǎn)數(shù)據(jù)之前獲取了
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
// 把當(dāng)前時(shí)間點(diǎn)變成展示狀態(tài)
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 // 第一次加載時(shí)間點(diǎn)數(shù)據(jù),后面的或條件可省略
) {
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 { // 此處是時(shí)間點(diǎn)被手動(dòng)關(guān)閉,手動(dòng)關(guān)閉會(huì)把showList中的數(shù)據(jù)清空,但是已經(jīng)加載過數(shù)據(jù)的情況
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: 頁面滾動(dòng)監(jiān)聽,用的是公司內(nèi)部的框架,就不展示html了,不同框架原理都是一樣的,只是需要寫的代碼多與少的區(qū)別,如ElementUI的InfiniteScroll,可以直接設(shè)置觸發(fā)加載的距離閾值
*/
handleScroll({ scrollTop, percentY }) { // 此處的scrollTop是組件返回的縱向滾動(dòng)的已滾動(dòng)距離,percentY則是已滾動(dòng)百分比
this.bus.$emit('scroll') // 觸發(fā)全局的滾動(dòng)監(jiān)聽,用于圖片的懶加載
this.scrolling = true
if (this.timer) { // 防抖機(jī)制,直至滾動(dòng)停止才會(huì)運(yùn)行定時(shí)器內(nèi)部內(nèi)容
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
requestAnimationFrame(async () => {
// 因?yàn)閮?nèi)部有觸發(fā)重排重繪,所以把代碼放在requestAnimationFrame中執(zhí)行
let height = window.innerHeight
if (
percentY > 0.7 && // 保證最開始的時(shí)候不要瘋狂加載,已滾動(dòng)70%再加載
Math.round(scrollTop / percentY) - scrollTop < height * 2 && // 保證數(shù)據(jù)量大后滾動(dòng)頁面長的時(shí)候不要瘋狂加載,在觸底小于兩倍視口高度的時(shí)候才加載
!this.isLoading // 保險(xiǎn),不同時(shí)運(yùn)行下面代碼,以防運(yùn)行時(shí)間大于定時(shí)時(shí)間
) {
this.isLoading = true
let len = this.list[this.lastListIndex].timeLines[
this.lastTimeIndex
].snapDetailList.length // list為一次性獲取所有數(shù)據(jù)存在內(nèi)存中
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
) { // 前端分批次展示完上一波數(shù)據(jù),該月份時(shí)間軸上下一個(gè)時(shí)間點(diǎn)存在的情況
await this.getTimelineData(
this.lastListIndex,
this.lastTimeIndex + 1
)
} else if (this.list.length > this.lastTimeIndex + 1) { // 前端分批次展示完上一波數(shù)據(jù),該月份時(shí)間軸上下一個(gè)時(shí)間點(diǎn)不存在,下一個(gè)月份存在的情況
await this.getTimelineData(this.lastListIndex + 1, 0)
}
}
this.$nextTick(() => {
this.isLoading = false
this.scrolling = false
})
})
}, 500)
},
結(jié)果:首屏渲染和事件響應(yīng)都變快了,只是滑動(dòng)到底部的時(shí)候有些許卡頓。
原因分析:滑動(dòng)到底部的卡頓,也是因?yàn)橐凰查g渲染一堆數(shù)據(jù),雖然比一次性展示所有的速度快很多,但是還是存在相比一次性展示不那么嚴(yán)重的重排和重繪,以及圖片不停加載渲染的情況。
3. 滾動(dòng)觸發(fā)+圖片懶加載
圖片懶加載可以解決每次渲染數(shù)據(jù)的時(shí)候因?yàn)閳D片按加載順序不停渲染產(chǎn)生的卡頓。
滾動(dòng)觸發(fā)使用點(diǎn)2的代碼。
提取通用的圖片組件,通過滾動(dòng)事件的全局觸發(fā),來控制每個(gè)數(shù)據(jù)項(xiàng)圖片的加載:
如上,點(diǎn)2中已經(jīng)在handleScroll中設(shè)置了 this.bus.$emit('scroll') // 觸發(fā)全局的滾動(dòng)監(jiān)聽,用于圖片的懶加載
// main.js Vue.prototype.bus = new Vue() ...
以下的在template中寫js不要學(xué)噢
// 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>
結(jié)果:在點(diǎn)2首屏展示快的基礎(chǔ)上,事件交互更快了,觸發(fā)展示數(shù)據(jù)也快了。
原因分析:防抖的圖片懶加載之后,只在用戶滾動(dòng)停止時(shí),加載視口內(nèi)的圖片,就沒有后續(xù)不斷的加載渲染圖片,也就不會(huì)因?yàn)椴煌d秩緢D片而影響事件交互和基礎(chǔ)的無圖卡片渲染。
以上一頓操作之后已經(jīng)符合本項(xiàng)目的需求了。
不過我研究了一下進(jìn)階操作 🤔
還可以只渲染視口元素,非視口用padding代替,以及把計(jì)算過程放在Web Worker多線程執(zhí)行,進(jìn)一步提升速度。
待我研究一下操作補(bǔ)上
以上就是js前端對于大量數(shù)據(jù)的展示方式及處理方法的詳細(xì)內(nèi)容,更多關(guān)于js 大量數(shù)據(jù)展示及處理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript中全局對象的parseInt()方法使用介紹
全局對象的parseInt()方法該如何使用,下面為大家詳細(xì)介紹下,感興趣的朋友不要錯(cuò)過2013-12-12
javascript檢測對象中是否存在某個(gè)屬性判斷方法小結(jié)
檢測對象中屬性的存在與否可以通過以下幾種方法來判斷:使用in關(guān)鍵字、使用對象的hasOwnProperty()方法、用undefined判斷、在條件語句中直接判斷,感興趣的朋友可以了解下哈2013-05-05
一個(gè)JavaScript遞歸實(shí)現(xiàn)反轉(zhuǎn)數(shù)組字符串的實(shí)例
這篇文章主要介紹了一個(gè)JavaScript遞歸實(shí)現(xiàn)反轉(zhuǎn)數(shù)組字符串的實(shí)例,很不錯(cuò),非常適合新手朋友們2014-10-10

