詳解JS如何解決大數(shù)據(jù)下滾動(dòng)頁(yè)面卡頓問(wèn)題
前言
之前遇到不分頁(yè)直接獲取到全部數(shù)據(jù),前端滾動(dòng)查看數(shù)據(jù),頁(yè)面就聽(tīng)卡頓的,當(dāng)然這和電腦瀏覽器性能啥的還是有點(diǎn)關(guān)系。但根源還是一次性渲染數(shù)據(jù)過(guò)多導(dǎo)致的,因此就想到解決這個(gè)問(wèn)題,最常見(jiàn)就是虛擬滾動(dòng),實(shí)現(xiàn)只渲染當(dāng)前可見(jiàn)的部分,這樣瀏覽器一次性渲染的數(shù)據(jù)少了。 本文介紹虛擬列表和虛擬Table的實(shí)現(xiàn),基于React + ts技術(shù)棧。
虛擬列表
虛擬列表通過(guò)僅渲染當(dāng)前可見(jiàn)區(qū)域的列表項(xiàng)來(lái)解決這個(gè)問(wèn)題。它利用瀏覽器的滾動(dòng)事件,根據(jù)用戶可見(jiàn)區(qū)域的大小和滾動(dòng)位置動(dòng)態(tài)地計(jì)算應(yīng)該渲染哪些列表項(xiàng)。這樣,即使數(shù)據(jù)量很大,也只有當(dāng)前可見(jiàn)的列表項(xiàng)會(huì)被渲染,大大減少了DOM元素的數(shù)量,提高了頁(yè)面性能和響應(yīng)性。 結(jié)合下圖想象一下
實(shí)現(xiàn)虛擬列表的方法主要涉及以下步驟:
- 計(jì)算可見(jiàn)區(qū)域:根據(jù)容器的尺寸(假如500px)和每一項(xiàng)的高度(50px),計(jì)算出可見(jiàn)的列表項(xiàng)數(shù)量。然后可視的數(shù)據(jù)就是10個(gè)。
- 監(jiān)聽(tīng)滾動(dòng)事件:在容器上添加滾動(dòng)事件監(jiān)聽(tīng),以便實(shí)時(shí)獲取滾動(dòng)位置。為了容器可滾動(dòng),需要在容器內(nèi)添加空的帶有高度的元素,將父元素?fù)伍_(kāi),然后可滾動(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; // 可見(jiàn)高度 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)聽(tīng) > {/* 可視數(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)前可見(jiàn)區(qū)域的表格單元格,以優(yōu)化性能并減少內(nèi)存占用。 在ant design4+的版本,也是給出了虛擬列表的實(shí)現(xiàn)方式的,基于‘react-window',大家也可以研究研究。我這里就是根據(jù)ant 提供的api components重寫(xiě)渲染的數(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; // 可見(jiàn)高度 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)簽,并通過(guò)style
設(shè)置高度為offset.bottom
。
其中,point
和offset
是通過(guò)其他邏輯計(jì)算得到的,可能是在組件的其他部分定義或使用的變量。
通過(guò)自定義components.body.wrapper
函數(shù),您可以對(duì)表格內(nèi)容進(jìn)行更加靈活的渲染和定制。在這種情況下,它主要用于實(shí)現(xiàn)虛擬表格的功能,只渲染可見(jiàn)區(qū)域的數(shù)據(jù)行,從而優(yōu)化大型表格的性能。
總結(jié)
本文只是實(shí)現(xiàn)了在固定每項(xiàng)列表高度的情況下的虛擬列表,現(xiàn)實(shí)很多情況是不定高的。這個(gè)比定高的復(fù)雜,不過(guò)原理也是一樣的,多了一步需要計(jì)算渲染后的實(shí)際高度的步驟。我也只是在項(xiàng)目中遇到了,寫(xiě)下來(lái)記錄方便后續(xù)查看。
以上就是詳解JS如何解決大數(shù)據(jù)下滾動(dòng)頁(yè)面卡頓問(wèn)題的詳細(xì)內(nèi)容,更多關(guān)于JS解決頁(yè)面卡頓的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript div 遮罩層封鎖整個(gè)頁(yè)面
在客戶端瀏覽器中,可以在某個(gè)時(shí)機(jī)使用javascript把一個(gè)div作為遮罩層,來(lái)封鎖整個(gè)頁(yè)面。2009-07-07Omi v1.0.2發(fā)布正式支持傳遞javascript表達(dá)式
這篇文章主要介紹了Omi v1.0.2發(fā)布正式支持傳遞javascript表達(dá)式,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-03-03JavaScript實(shí)現(xiàn)動(dòng)態(tài)添加Form表單元素的方法示例
這篇文章主要介紹了JavaScript實(shí)現(xiàn)動(dòng)態(tài)添加Form表單元素的方法,結(jié)合實(shí)例形式分析了javascript表單元素操作相關(guān)函數(shù)使用方法與相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-08-08JavaScript開(kāi)發(fā)簡(jiǎn)單易懂的Svelte實(shí)現(xiàn)原理詳解
這篇文章主要為大家介紹了JavaScript開(kāi)發(fā)簡(jiǎn)單易懂的Svelte實(shí)現(xiàn)原理的內(nèi)容詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-11-11javascript 網(wǎng)站常用的iframe分割
就是一個(gè)頁(yè)面使用兩個(gè)iframe來(lái)調(diào)用內(nèi)容,實(shí)現(xiàn)頁(yè)面導(dǎo)航,更容易控制,可控制性好2008-06-06