詳解JS如何解決大數(shù)據(jù)下滾動(dòng)頁面卡頓問題
前言
之前遇到不分頁直接獲取到全部數(shù)據(jù),前端滾動(dòng)查看數(shù)據(jù),頁面就聽卡頓的,當(dāng)然這和電腦瀏覽器性能啥的還是有點(diǎn)關(guān)系。但根源還是一次性渲染數(shù)據(jù)過多導(dǎo)致的,因此就想到解決這個(gè)問題,最常見就是虛擬滾動(dòng),實(shí)現(xiàn)只渲染當(dāng)前可見的部分,這樣瀏覽器一次性渲染的數(shù)據(jù)少了。 本文介紹虛擬列表和虛擬Table的實(shí)現(xiàn),基于React + ts技術(shù)棧。
虛擬列表
虛擬列表通過僅渲染當(dāng)前可見區(qū)域的列表項(xiàng)來解決這個(gè)問題。它利用瀏覽器的滾動(dòng)事件,根據(jù)用戶可見區(qū)域的大小和滾動(dòng)位置動(dòng)態(tài)地計(jì)算應(yīng)該渲染哪些列表項(xiàng)。這樣,即使數(shù)據(jù)量很大,也只有當(dāng)前可見的列表項(xiàng)會(huì)被渲染,大大減少了DOM元素的數(shù)量,提高了頁面性能和響應(yīng)性。 結(jié)合下圖想象一下

實(shí)現(xiàn)虛擬列表的方法主要涉及以下步驟:
- 計(jì)算可見區(qū)域:根據(jù)容器的尺寸(假如500px)和每一項(xiàng)的高度(50px),計(jì)算出可見的列表項(xiàng)數(shù)量。然后可視的數(shù)據(jù)就是10個(gè)。
- 監(jiān)聽滾動(dòng)事件:在容器上添加滾動(dòng)事件監(jiān)聽,以便實(shí)時(shí)獲取滾動(dòng)位置。為了容器可滾動(dòng),需要在容器內(nèi)添加空的帶有高度的元素,將父元素?fù)伍_,然后可滾動(dòng)。獲取scrollTop的高度,就能計(jì)算出當(dāng)前顯示第一項(xiàng)的下標(biāo)(scrollTop / itemHeight),動(dòng)態(tài)更新數(shù)據(jù)。
基于上面的思路,封裝一個(gè)滾動(dòng)列表組件。
import _ from "lodash";
import React, { useEffect, useState } from "react";
import { listData } from "./data";
type ListType = {
itemHeight?: number; // 每一項(xiàng)的高度
visibleHeight?: number; // 可見高度
total?: number; // 數(shù)據(jù)總數(shù)
dataSource?: any[]; // 全部數(shù)據(jù)
};
// 為了看效果我模擬的數(shù)據(jù)
const myList = Array.from(Array(1000), (item, index) => ({name: `名字${item}`, id: index}));
const List = (props: ListType) => {
const {
itemHeight = 54,
visibleHeight = 540,
total = 130,
dataSource = myList,
} = props;
const [showData, setShowData] = useState<any>([]);
const [offset, setOffset] = useState<any>({ top: 0, bottom: 0 });
const visibleCount = Math.ceil(visibleHeight / itemHeight);
useEffect(() => {
const list = _.slice(dataSource, 0, visibleCount);
const bottom = (total - visibleCount) * itemHeight;
setOffset({ top: 0, bottom });
setShowData(list);
}, [dataSource]);
const onScroll = (event: React.UIEvent<HTMLDivElement>) => {
const target = event.currentTarget;
const startIdx = Math.floor(target.scrollTop / itemHeight);
const endIdx = startIdx + visibleCount;
setShowData(dataSource.slice(startIdx, endIdx));
const top = startIdx * itemHeight;
const bottom = (total - endIdx) * itemHeight;
setOffset({ top, bottom });
};
return (
<div
className="virtual"
style={{
height: visibleHeight,
width: "100%",
overflow: "auto",
border: "1px solid #d9d9d9",
}}
onScroll={onScroll} // 在父元素上添加滾動(dòng)事件監(jiān)聽
>
{/* 可視數(shù)據(jù) 為了滾動(dòng)數(shù)據(jù)一直在可視區(qū)。加上頂部偏移 */}
<div style={{ height: visibleHeight, marginTop: offset.top }}>
{_.map(showData, (item, index: any) => {
return (
<div
style={{
display: "flex",
alignItems: "center",
height: itemHeight,
borderBottom: "1px solid #d9d9d9",
}}
key={index}
>
{item.name}
</div>
);
})}
</div>
{/* 底部占位 */}
<div style={{ height: offset.bottom }} />
</div>
);
};
export default List;虛擬Table
虛擬表格和虛擬列表的思路差不多,是虛擬列表的一種特殊形式,通常用于處理大型的表格數(shù)據(jù)。類似于虛擬列表,虛擬表格也只渲染當(dāng)前可見區(qū)域的表格單元格,以優(yōu)化性能并減少內(nèi)存占用。 在ant design4+的版本,也是給出了虛擬列表的實(shí)現(xiàn)方式的,基于‘react-window',大家也可以研究研究。我這里就是根據(jù)ant 提供的api components重寫渲染的數(shù)據(jù);獲取到可視區(qū)起點(diǎn)和終點(diǎn)下標(biāo),然后只展示當(dāng)前可視的數(shù)據(jù)。 思路和上面的列表基本一樣,直接上代碼
import React, { useEffect, useRef, useState } from "react";
import { Table } from "antd";
import _ from "lodash";
type TableType = {
itemHeight?: number; // 每一項(xiàng)的高度
visibleHeight?: number; // 可見高度
total?: number; // 數(shù)據(jù)總數(shù)
dataSource?: any[]; // 全部數(shù)據(jù)
};
// 為了看效果我模擬的數(shù)據(jù)
const myList = Array.from(Array(1000), (item, index) => ({name: `名字${item}`, id: index}));
const VirtualTable = (props: TableType) => {
const {
itemHeight = 54,
visibleHeight = 540,
total = 130,
dataSource = myList,
} = props;
const [point, setPoint] = useState<any>([0, 20]);
const [offset, setOffset] = useState<any>({top:0, bottom: 0 });
const tabRef = useRef<any>();
const containRef = useRef<any>();
const visibleCount = Math.ceil(visibleHeight / itemHeight);
useEffect(() => {
const bottom = (total - visibleCount) * itemHeight;
setOffset({ bottom });
setPoint([0, visibleCount]);
const scrollDom = tabRef?.current?.querySelector(".ant-table-body");
console.log("aaa",scrollDom);
if (scrollDom) {
containRef.current = scrollDom;
containRef.current.addEventListener("scroll", onScroll);
return () => {
containRef.current.removeEventListener("scroll", onScroll);
};
}
}, [myList]);
const onScroll = (e: any) => {
const startIdx = Math.floor(e?.target?.scrollTop / itemHeight);
const endIdx = startIdx + visibleCount;
const bottom = (total - endIdx) * itemHeight;
const top = startIdx * itemHeight;
setOffset({top,bottom});
setPoint([startIdx, endIdx]);
};
const columns = [
{ title: "ID", dataIndex: "id", width: 150 },
{ title: "名字", dataIndex: "name", width: 150 },
];
return (
<Table
ref={tabRef}
className="virtual-table"
pagination={false}
columns={columns}
dataSource={dataSource}
scroll={{ y: visibleHeight }}
components={{
body: {
wrapper: ({ className, children }: any) => {
return (
<tbody className={className}>
{children?.[0]}
<tr style={{height: offset.top}}/>
{_.slice(children?.[1], point?.[0], point?.[1])}
<tr style={{height: offset.bottom}}></tr>
</tbody>
);
},
},
}}
/>
);
};
export default VirtualTable;在上面的代碼里,用到Ant Design的Table組件中的components.body.wrapper定制表格內(nèi)容區(qū)域的包裝器組件。它的作用是對(duì)表格的內(nèi)容進(jìn)行包裝,并且可以自定義一些顯示邏輯。components.body.wrapper函數(shù)接收一個(gè)對(duì)象參數(shù),其中包含以下參數(shù):
className: 傳遞給tbody標(biāo)簽的類名。它是一個(gè)字符串,包含了tbody標(biāo)簽的類名,可以用于自定義樣式。children: 表格的內(nèi)容區(qū)域的子元素,即表格的數(shù)據(jù)行和列。
在給定的代碼中,components.body.wrapper函數(shù)接收了一個(gè)參數(shù)對(duì)象,該對(duì)象包含className和children屬性。在函數(shù)內(nèi)部,它會(huì)將children分割成三部分:
children?.[0]:這是表格的標(biāo)題行,即表頭部分,對(duì)應(yīng)于<thead>標(biāo)簽。{_.slice(children?.[1], point?.[0], point?.[1])}:這是表格的數(shù)據(jù)行,根據(jù)point的取值進(jìn)行了切片,只渲染point范圍內(nèi)的數(shù)據(jù)行,對(duì)應(yīng)于<tr>標(biāo)簽。<tr style={{height: offset.bottom}}></tr>:這是底部占位行,用于確保在滾動(dòng)時(shí)能正確顯示表格的底部?jī)?nèi)容,對(duì)應(yīng)于<tr>標(biāo)簽,并通過style設(shè)置高度為offset.bottom。
其中,point和offset是通過其他邏輯計(jì)算得到的,可能是在組件的其他部分定義或使用的變量。
通過自定義components.body.wrapper函數(shù),您可以對(duì)表格內(nèi)容進(jìn)行更加靈活的渲染和定制。在這種情況下,它主要用于實(shí)現(xiàn)虛擬表格的功能,只渲染可見區(qū)域的數(shù)據(jù)行,從而優(yōu)化大型表格的性能。
總結(jié)
本文只是實(shí)現(xiàn)了在固定每項(xiàng)列表高度的情況下的虛擬列表,現(xiàn)實(shí)很多情況是不定高的。這個(gè)比定高的復(fù)雜,不過原理也是一樣的,多了一步需要計(jì)算渲染后的實(shí)際高度的步驟。我也只是在項(xiàng)目中遇到了,寫下來記錄方便后續(xù)查看。
以上就是詳解JS如何解決大數(shù)據(jù)下滾動(dòng)頁面卡頓問題的詳細(xì)內(nèi)容,更多關(guān)于JS解決頁面卡頓的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
js判斷iframe內(nèi)的網(wǎng)頁是否滾動(dòng)到底部觸發(fā)事件
這篇文章主要介紹了js判斷iframe內(nèi)的網(wǎng)頁是否滾動(dòng)到底部觸發(fā)事件,需要的朋友可以參考下2014-03-03
分享一個(gè)自己寫的table表格排序js插件(高效簡(jiǎn)潔)
在前不久做的一個(gè)web項(xiàng)目中,需要實(shí)現(xiàn)js表格排序的效果,當(dāng)時(shí)為了省事,就在網(wǎng)上找了幾個(gè)相關(guān)的js插件2011-10-10
js實(shí)現(xiàn)購(gòu)物車計(jì)算的方法
這篇文章主要為大家詳細(xì)介紹了js實(shí)現(xiàn)購(gòu)物車的計(jì)算方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
JavaScript實(shí)現(xiàn)頁面實(shí)時(shí)顯示當(dāng)前時(shí)間的簡(jiǎn)單實(shí)例
這篇文章介紹了頁面實(shí)時(shí)顯示當(dāng)前時(shí)間的簡(jiǎn)單實(shí)例,有需要的朋友可以參考需要2013-07-07
JavaScript解析機(jī)制與閉包原理實(shí)例詳解
這篇文章主要介紹了JavaScript解析機(jī)制與閉包原理,結(jié)合實(shí)例形式詳細(xì)分析了javascript解析機(jī)制相關(guān)概念、功能、用法以及閉包的原理、定義、使用方法,需要的朋友可以參考下2019-03-03
細(xì)說JS數(shù)組遍歷的一些細(xì)節(jié)及實(shí)現(xiàn)
本文主要介紹了細(xì)說JS數(shù)組遍歷的一些細(xì)節(jié)及實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
JS關(guān)鍵字球狀旋轉(zhuǎn)效果的實(shí)例代碼
這篇文章主要介紹了JS關(guān)鍵字球狀旋轉(zhuǎn)效果的實(shí)例代碼。需要的朋友可以過來參考下,希望對(duì)大家有所幫助2013-11-11

