花三個小時,完全掌握分片渲染和虛擬列表~
大家好,我是 Domesy,掘金:小杜杜,有關高性能,大數據量的列表渲染的示例已經非常常見,可以說是前端必須要了解的功能點,今天我們一起手寫一下,看看如何去更好的實現~
我們知道有些場景下,接口會返回出大量的數據,渲染這種列表叫做 長列表
, 今天主要說下處理 長列表
的兩種方式:分片渲染
和 虛擬列表
,請各位小夥伴多多支持~
在正式開始前,希望各位小夥伴牢牢記住:js 執行永遠要比 dom 快的多,所以對於執行大量的數據,一次性渲染,非常容易造成卡頓、卡死的情況
疑問點
我們先來看看以下代碼:
import React,{ useState } from 'react';
import { Button } from 'antd-mobile';
import img from './img.jpeg'
// 子組件
const Item:React.FC<{id: number, waitRender?: () => void}> = ({id, waitRender}) => {
return (
<div style={{display: 'flex', alignItems: 'center', marginBottom: 5}}>
<img src={img} width={80} height={60} alt="" />列表{id}
</div>
)
}
const Index:React.FC<any> = (props)=> {
const [flag, setFalag] = useState<boolean>(false)
const [list, setList] = useState<Array<number>>([])
return (
<div>
<Button onClick={async () => {
setFalag(true)
let arr:number[] = []
console.time()
for(let i = 0; i < 5000; i++){
arr.push(i)
}
await setList(arr)
console.timeEnd()
}} >渲染</Button>
{
flag && list.map((item) => <Item id={item} key={item} />)
}
</div>
);
}
export default Index;
這裏的 Item
是我們的子組件,也就是一行一行的數據,爲了大家更好的看到事件,我特意做了個按鈕來控制列表的長度,這裏我們假設有五萬條數據,通過 console.time()
和 console.timeEnd()
計算一下加載這五萬條數據需要多長時間?
可以看到加載的時間大概爲 2.7s
,這樣的速度明顯達不到要求,而且在真實情況下很容易出現白屏,卡頓的情況,這明顯不是我們想要的情況~
分片渲染
分片渲染:簡單的說就是一個執行完再執行下一個,其思想是建立一個隊列,通過定時器來進行渲染,比如說一共有 3 次,先把這三個放入到數組中,當第一個執行完成後,並剔除執行完成的,在執行第二個,直到全部執行完畢,渲染隊列清空。
利用定時器
我們可以通過設置定時器來進行渲染,通過設置一個 等待隊列(waitList)
和 是否渲染(isRender)
的條件去做。(在這裏我把定時器的時間設置爲 500,方便演示)
HOC
import { useEffect, useState } from 'react';
import { DotLoading } from 'antd-mobile';
const waitList:any = [] //等待隊列
let isRender:boolean = false //控制渲染條件
const waitRender = () => {
const res = waitList.shift()
if(!res) return
setTimeout(() => {
res()
}, 500) //爲演示效果加入一個延長時間
}
const HOC = (Component:any) => (props:any) => {
const [show, setShow] = useState<boolean>(false)
useEffect(() => {
waitList.push(() => {setShow(true)})
if(!isRender){
waitRender()
isRender = true
}
}, [])
return show ? <Component waitRender={waitRender} {...props}/> : <div style={{margin: 25}}><DotLoading color='primary' />加載中</div>
}
export default HOC;
代碼示例:
import React,{ useEffect, useState } from 'react';
import img from './img.jpeg'
import { SlicingHoc } from '@/components';
// 子組件
const Item:React.FC<{id: number, waitRender: () => void}> = ({id, waitRender}) => {
useEffect(() => {
waitRender()
}, [])
return (
<div style={{display: 'flex', alignItems: 'center', padding: 5}}>
<img src={img} width={80} height={60} alt="" />列表{id}
</div>
)
}
const ItemHoc = SlicingHoc(Item)
const Index:React.FC<any> = (props)=> {
const [list, setList] = useState<Array<number>>([])
useEffect(() => {
let arr:number[] = []
for(let i = 0; i < 5000; i++){
arr.push(i)
}
setList(arr)
}, [])
return (
<div>
{
list.map((item) => <ItemHoc id={item} key={item} />)
}
</div>
);
}
export default Index;
效果:
就能實現這樣的效果,這個主要有以下兩個缺點:
-
在這個組件中,我們需要通過
HOC
傳遞一個waitRender()
方法來記錄整個隊列,也就是說我們要用這個高階組件,還必須要改變字組件的結構,這點並不是特別好 -
我們發現這種情況是進行一個一個渲染,上面的沒有執行完,下面的是不會渲染的,也就會造成如下效果(定時器設置爲 0 也一樣)如:
進行改造
分析:
- 針對上述的第一點,我們可以把裏面的數組包裝成一個整體,通過
HOC
去循環 2. 針對第二點,我們沒有必要一個一個進行渲染,可以一次渲染 100 個,這樣渲染的速度就會加快
HOC:
import { useEffect, useState } from 'react';
let waitList:any = [] //等待隊列
const HOC = (Component:any) => ({list, ...props}:any) => {
const [data, setData] = useState<any>([])
useEffect(() => {
if(list.length !== 0){
sliceTime(list, 0)
}
}, [list])
const sliceTime = (list:any[], times = 0, number:number = 100) => {
if(times === (Math.ceil(list.length / number) + 1)) return //判斷條件
setTimeout(() => {
const newList:any = list.slice(times * number, (times + 1) * number)
waitList = [...waitList, ...newList]
setData(waitList)
sliceTime(list, times + 1)
}, 500);
}
if(list.length === 0) return <></>
return <>{
data.map((item:any) => <Component id={item} {...props} key={item} />)
}</>
}
export default HOC;
代碼展示:
import React,{ useEffect, useState } from 'react';
import img from './img.jpeg'
import { SlicingHoc } from '@/components';
// 子組件
const Item:React.FC<{id: any}> = ({id}) => {
return (
<div style={{display: 'flex', alignItems: 'center', padding: 5}}>
<img src={img} width={80} height={60} alt="" />列表{id}
</div>
)
}
const ItemHoc = SlicingHoc(Item)
const Index:React.FC<any> = (props)=> {
const [list, setList] = useState<Array<number>>([])
useEffect(() => {
let arr:number[] = []
for(let i = 0; i < 50000; i++){
arr.push(i)
}
setList(arr)
}, [])
return (
<div>
<ItemHoc list={list} />
</div>
);
}
export default Index;
效果:
此時,你就會發現渲染的速度會快很多,也沒有太大的卡頓,效果而言還算不錯~
當然,你可以根據實際情況去設置一次渲染的次數,把
HOC
定製爲公共化,具體的操作可以參考一下這篇文章:作爲一名 React,我是這樣理解 HOC 的 - 定製爲公用 HOC
虛擬列表
我們發現分片渲染有一個根本問題,就是依次渲染,將龐大的數據切分開,然後按順序依次渲染
但大多數人進入到列表頁面,根本不會將整個列表全部看完,從某種角度上來說,像這種全部渲染的情況比較雞肋,所以在大多數情況下,會採取虛擬列表的形式
虛擬列表:實際上是一種實現方案,只對 可視區域
進行渲染,對 非可視區域
中的區域不渲染或只渲染一部分(渲染的部分叫 緩衝區
,不渲染的部分叫 虛擬區
),從而達到極高的性能
簡單分析
我們先看一下下方的圖(由於我的圖畫的實在難看,所以在網上找了一張比較符合的,還望勿噴~)
從圖中可以看出,我們可以將列表分爲三個區域:可視區、緩衝區、虛擬區
而我們主要針對 可視區
和 緩衝取
進行渲染,我們一步一步的實現,有不對的地方,希望在評論區指出~
頁面佈局
在這個組件中,首先要做的事情就是佈局,我們需要有兩塊區域:
-
佔位區域
:聰明的小夥伴發現,在上述的分片渲染
中,滾動條也在變化,這是因爲列表渲染的數據在增加,把內容組件撐開,造成高度上的變化,所以在虛擬列表中,專門提供一個 div,用來佔位,這樣在一進來的時候滾動條就不會產生變化 -
渲染區域
:這塊部分爲真正用戶看到的列表區域,實際上有可視區和緩衝區共同組成,緩衝區的作用是防止快速下滑或者上滑的過程中
出現空白區域
其次我們需要一個整體的 div
,通過監聽 佔位區域
的滾動條,判斷當前截取數組的區域,所以大體的結構是這樣
<div ref={allRef}>
<div
ref={scrollRef}
>
{/* 佔位,列表的總高度,用於生成滾動條 */}
<div></div>
{/* 內容區域 */}
<div>
{/* 渲染區域 */}
{
state.data.map((item:any) => <div key={item}>
{/* 子組件 */}
<Component id={item} {...props}/>
</div>)
}
</div>
</div>
</div>
參數計算
我們可以將需要的元素進行總結,在這裏,我會使用 useReactive
來存取參數,是一種具備 響應式
的 useState
,關於這個的實現,可參考:搞懂這 12 個 Hooks,保證讓你玩轉 React-useReactive
相關容器的高度:
- 列表總共的個數(總列表數):
設置爲 list
容器的高度: 當前組件所佔的位置(可通過傳值控制) scrollAllHeight = allRef.current.offsetHeight
子列表高度:子組件的高度(這個如何獲取,後續講到,案例中爲 65) ItemHeight = 65
佔位區域高度,也就是整個列表的高度,用於生成滾動條:佔位區域高度 = 子列表高度 * 列表總數個數
listHeight = ItemHeight * list.length
渲染區域的計算點:其實我們渲染的數據只是可視區和緩衝區,我們可以利用 slice
對 list
進行截取,所以在我們還需要知道:
-
索引的起始位置:start
-
索引的結束位置:end
-
緩衝個數:bufferCount
-
需要渲染的節點數量(可視區能渲染幾個節點)
渲染節點的數量 = 容器的高度 / 子列表高度 (需要向上取整) + 緩衝個數
const renderCount = Math.ceil(scrollAllHeight / ItemHeight)+ state.bufferCount
滾動區域
在這裏我使用 useEventListener
去監聽滾動事件,是一個可以監聽任何函數的自定義 hooks,具體實現可參考:搞懂這 12 個 Hooks,保證讓你玩轉 React-useEventListener
我們要拿到滾動條距離頂部的高度,然後計算對應的索引 起始
和 結束
位置,再截取對應的數據給到 data
就 OK 了,並且計算對應的 偏移量
, 也就是:
useEventListener('scroll', () => {
// 頂部高度
const { scrollTop } = scrollRef.current
state.start = Math.floor(scrollTop / state.itemHeight)
state.end = Math.floor(scrollTop / state.itemHeight + state.renderCount + 1)
state.currentOffset = scrollTop - (scrollTop % state.itemHeight)
state.data = list.slice(state.start, state.end)
}, scrollRef)
優化
經過上面的講解,我們可以發現,高階組件渲染的數據實際上只有 state.data
,數據的變化是由滾動事件所引起的,造成 start
和 end
的改變,所以在這裏,我們可以使用 useCreation
來進行優化, useCreation
相當於是升級版的 useMemo
,具體實現,可以參考搞懂這 12 個 Hooks,保證讓你玩轉 React-useCreation
useCreation(() => {
state.data = list.slice(state.start, state.end)
}, [state.start])
這樣,一個簡易版的 虛擬列表
就 ok 了
代碼展示
HOC:
import { useEffect, useRef } from 'react';
import useReactive from '../useReactive'
import useEventListener from '../useEventListener'
import useCreation from '../useCreation'
const HOC = (Component:any) => ({list, ...props}:any) => {
const state = useReactive({
data: [], //渲染的數據
scrollAllHeight: '100vh', // 容器的初始高度
listHeight: 0, //列表高度
itemHeight: 0, // 子組件的高度
renderCount: 0, // 需要渲染的數量
bufferCount: 6, // 緩衝的個數
start: 0, // 起始索引
end: 0, // 終止索引
currentOffset: 0, // 偏移量
})
const allRef = useRef<any>(null) // 容器的ref
const scrollRef = useRef<any>(null) // 檢測滾動
useEffect(() => {
// 子列表高度
const ItemHeight = 65
// 容器的高度
const scrollAllHeight = allRef.current.offsetHeight
// 列表高度
const listHeight = ItemHeight * list.length;
//渲染節點的數量
const renderCount = Math.ceil(scrollAllHeight / ItemHeight) + state.bufferCount
state.renderCount = renderCount
state.end = renderCount + 1
state.listHeight = listHeight
state.itemHeight = ItemHeight
state.data = list.slice(state.start, state.end)
}, [allRef])
useCreation(() => {
state.data = list.slice(state.start, state.end)
}, [state.start])
useEventListener('scroll', () => {
// 頂部高度
const { scrollTop } = scrollRef.current
state.start = Math.floor(scrollTop / state.itemHeight)
state.end = Math.floor(scrollTop / state.itemHeight + state.renderCount + 1)
state.currentOffset = scrollTop - (scrollTop % state.itemHeight)
// state.data = list.slice(state.start, state.end)
}, scrollRef)
return <div ref={allRef}>
<div
style={{height: state.scrollAllHeight, overflow: 'scroll', position: 'relative'}}
ref={scrollRef}
>
{/* 佔位,列表的總高度,用於生成滾動條 */}
<div style={{ height: state.listHeight, position: 'absolute', left: 0, top: 0, right: 0 }}></div>
{/* 內容區域 */}
<div style={{ transform: `translate3d(0, ${state.currentOffset}px, 0)`, position: 'relative', left: 0, top: 0, right: 0}}>
{/* 渲染區域 */}
{
state.data.map((item:any) => <div key={item}>
{/* 子組件 */}
<Component id={item} {...props} />
</div>)
}
</div>
</div>
</div>
}
export default HOC;
頁面代碼
import React,{ useEffect, useState } from 'react';
import img from './img.jpeg'
import { HOC } from '@/components';
// 子組件
const Item:React.FC<{id: any}> = ({id}) => {
return (
<div style={{display: 'flex', alignItems: 'center', padding: 5}}>
<img src={img} width={80} height={60} alt="" />列表{id}
</div>
)
}
const ItemHoc = HOC(Item)
const Index:React.FC<any> = (props)=> {
const [list, setList] = useState<Array<number>>([])
useEffect(() => {
let arr:number[] = []
for(let i = 0; i < 500; i++){
arr.push(i)
}
setList(arr)
}, [])
if(list.length === 0) return <></>
return (
<div>
<ItemHoc list={list} />
</div>
);
}
export default Index;
效果
虛擬列表 - 可優化的方向
下拉請求數據
海量的數據可能用戶並不會看完,需要下拉到底部進行刷新,所以我們可以判斷一個臨界值:滾動條距離底部的距離爲 0 時出發
臨界值:距離底部的高度 = 滾動條的高度 - 默認的高度 - 距離頂部的高度
const button = scrollHeight - clientHeight - scrollTop
然後我們傳遞給外界一個方法,做請求事件即可:onRequest(請求完拼接到 list 即可)
useEventListener('scroll', () => {
// 頂部高度
const { clientHeight, scrollHeight } = scrollRef.current
// 滾動條距離的高度
const button = scrollHeight - clientHeight - scrollTop
if(button === 0 && onRequest){
onRequest()
}
}, scrollRef)
效果:
子列表高度問題
在上面的代碼中,其實有一個很重要的問題,就是子列表的高度,我在上述的過程中,實際上是寫死的
但在實際的開發過程中,子列表的高度有兩種情況:定高
和 不定高
定高
很簡單,我們只需要手動計算下列表的高度,將值傳入就行,但 不定高
就很麻煩了,因爲你無法計算出每個高度的情況,導致 列表的整體高度
、 偏移量
都無法正常的計算
在這裏我用 mock
來模擬些數據看看:
思考
對於子列表的動態高度我們該如何處理?
-
第一種,將
ItemHeight
作爲參數傳遞過來,我們可以根據傳遞數組
來控制,但這種情況需要我們提前將列表的高度算出來,算每個子列表的高度很麻煩,其次這個高度還要根據屏幕的大小去變化,這個方法明顯不適合 -
第二種,
預算高度
,我們可以假定子列表的高度也就是虛假高度(initItemHeight
), 當我們渲染的時候,在更新對應高度,這樣就可以解決子列表高度的問題
預算高度該如何考慮
針對第二種方案,我們需要去維護一個公共的高度列表( positions
),這個數組將會記錄真實的 DOM 高度
那麼 positions
需要記錄那些信息:
const state = useReactive<any>({
...,
positions: [ //需要記錄每一項的高度
// index // 當前pos對應的元素的下標
// top; // 頂部位置
// bottom // 底部位置
// height // 元素高度
// dHeight // 用於判斷是否需要改變
],
initItemHeight: 50, // 預計高度
})
需要記錄 元素的高度
,其次可以存入距離頂部和底部的高度,方便後面計算偏移量和列表的整體高度,在設定一個參數( dHeight
)判斷新的高度與舊的高度是否一樣,不一樣的話就進行更新
其中最重要的就是 index,它用來記錄子列表真實高度的下標,這個點極爲重要,原因是:在之前的講解中,我們發現 start
和 end
的差值實際上是不變的,也就是說,最終渲染的數據,實際上是一個 固定值
,但裏面的子列表高度卻是 變值
, 所以我們需要有一個變量來區分數據所對應的高度,所以這個 index
就變的尤爲重要
所以在這裏我們設置一個 ref
用來監聽子節點 node
,來獲取真實高度, 這裏我設置 id 來判斷對應的索引
// list:數據改變
let arr:any[] = []
for(let i = 0; i < 100; i++){
arr.push({
id: i, //設置唯一值
content: Mock.mock('@csentence(40, 100)') // 內容
})
}
setList(arr)
// 渲染數據
{/* 內容區域 */}
<div ref={ref} style={{ transform: `translate3d(0, ${state.currentOffset}px, 0)`, position: 'relative', left: 0, top: 0, right: 0}}>
{/* 渲染區域 */}
{
state.data.map((item:any) => <div id={String(item.id)} key={item.id}>
{/* 子組件 */}
<Component id={item.content} {...props} index={item.id} />
</div>)
}
</div>
//初始的positions
useEffect(() => {
// 初始高度
initPositions()
}, [])
const initPositions = () => {
const data = []
for (let i = 0; i < list.length; i++) {
data.push({
index: i,
height: state.initItemHeight,
top: i * state.initItemHeight,
bottom: (i + 1) * state.initItemHeight,
dHeight: 0
})
}
state.positions = [...data]
}
初始計算
我們要改變的是 子列表的高度
和 列表的高度
useEffect(() => {
// 子列表高度:爲默認的預計高度
const ItemHeight = state.initItemHeight
// // 容器的高度
const scrollAllHeight = allRef.current.offsetHeight
// 列表高度:positions最後一項的bottom
const listHeight = state.positions[state.positions.length - 1].bottom;
//渲染節點的數量
const renderCount = Math.ceil(scrollAllHeight / ItemHeight)
state.renderCount = renderCount
state.end = renderCount + 1
state.listHeight = listHeight
state.itemHeight = ItemHeight
state.data = list.slice(state.start, state.end)
}, [allRef, list.length])
這裏要注意一點的是:預計高度儘量要小點,可以多加載,但不能少,防止渲染不全
更新具體的高度
當我們第一遍把列表的數據渲染成功後,就更新positions
的高度,將真實的高度替換一開始的虛擬高度,並將整體的高度進行更新
useEffect(() => {
setPostition()
}, [ref.current])
const setPostition = () => {
const nodes = ref.current.childNodes
if(nodes.length === 0) return
nodes.forEach((node: HTMLDivElement) => {
if (!node) return;
const rect = node.getBoundingClientRect(); // 獲取對應的元素信息
const index = +node.id; // 可以通過id,來取到對應的索引
const oldHeight = state.positions[index].height // 舊的高度
const dHeight = oldHeight - rect.height // 差值
if(dHeight){
state.positions[index].height = rect.height //真實高度
state.positions[index].bottom = state.positions[index].bottom - dHeight
state.positions[index].dHeight = dHeight //將差值保留
}
});
// 重新計算整體的高度
const startId = +nodes[0].id
const positionLength = state.positions.length;
let startHeight = state.positions[startId].dHeight;
state.positions[startId].dHeight = 0;
for (let i = startId + 1; i < positionLength; ++i) {
const item = state.positions[i];
state.positions[i].top = state.positions[i - 1].bottom;
state.positions[i].bottom = state.positions[i].bottom - startHeight;
if (item.dHeight !== 0) {
startHeight += item.dHeight;
item.dHeight = 0;
}
}
// 重新計算子列表的高度
state.itemHeight = state.positions[positionLength - 1].bottom;
}
這樣就可以將真實的高度
替換虛擬的高度
除了首次的渲染之外,還有就是在start
或end
改變時重新計算,也就是
js
useCreation(() => { state.data = list.slice(state.start, state.end)
if(ref.current){
setPostition()
}
}, [state.end])
計算偏移量
在滾動的方法中,我們可以通過二分查找
去降低檢索次數,同時我們每次的偏移量爲state.positions[state.start - 1].bottom
tsx
useEventListener('scroll', () => {
// 頂部高度
const { scrollTop, clientHeight, scrollHeight } = scrollRef.current
state.start = binarySearch(state.positions, scrollTop);
state.end = state.start + state.renderCount + 1
// 計算偏移量
state.currentOffset = state.start > 0 ? state.positions[state.start - 1].bottom : 0
// 滾動條距離的高度
const button = scrollHeight - clientHeight - scrollTop
if(button === 0 && onRequest){
onRequest()
}
}, scrollRef)
// 二分查找
const binarySearch = (list:any[], value: any) =>{ let start:number = 0; let end:number = list.length - 1; let tempIndex = null; while(start <= end){ let midIndex = parseInt(String( (start + end)/2)); let midValue = list[midIndex].bottom; if(midValue === value){ return midIndex + 1; }else if(midValue < value){ start = midIndex + 1; }else if(midValue > value){ if(tempIndex === null || tempIndex > midIndex){ tempIndex = midIndex; } end = end - 1; } } return tempIndex; }
代碼展示
HOC:
jsx import {useEffect, useRef} from 'react'; import useReactive from '../useReactive' import useEventListener from '../useEventListener' import useCreation from '../useCreation'
const HOC = (Component:any) => ({list, onRequest, ...props}:any) => {
const state = useReactive({data: [], // 渲染的數據 scrollAllHeight: '100vh', // 容器的初始高度 listHeight: 0, // 列表高度 itemHeight: 0, // 子組件的高度 renderCount: 0, // 需要渲染的數量 bufferCount: 6, // 緩衝的個數 start: 0, // 起始索引 end: 0, // 終止索引 currentOffset: 0, // 偏移量 positions: [ // 需要記錄每一項的高度 // index // 當前 pos 對應的元素的下標 // top; // 頂部位置 // bottom // 底部位置 // height // 元素高度 // dHeight // 用於判斷是否需要改變 ], initItemHeight: 50, // 預計高度 })
const allRef = useRef(null) // 容器的 ref const scrollRef = useRef(null) // 檢測滾動 const ref = useRef(null) // 檢測滾動
useEffect(() => { // 初始高度 initPositions() }, [])
const initPositions = () => { const data = [] for (let i = 0; i < list.length; i++) { data.push({ index: i, height: state.initItemHeight, top: i * state.initItemHeight, bottom: (i + 1) * state.initItemHeight, dHeight: 0 }) } state.positions = [...data] }
useEffect(() => {
// 子列表高度:爲默認的預計高度
const ItemHeight = state.initItemHeight
// // 容器的高度
const scrollAllHeight = allRef.current.offsetHeight
// 列表高度:positions最後一項的bottom
const listHeight = state.positions[state.positions.length - 1].bottom;
//渲染節點的數量
const renderCount = Math.ceil(scrollAllHeight / ItemHeight)
state.renderCount = renderCount
state.end = renderCount + 1
state.listHeight = listHeight
state.itemHeight = ItemHeight
state.data = list.slice(state.start, state.end)
}, [allRef, list.length])
useEffect(() => { setPostition() }, [ref.current])
const setPostition = () => { const nodes = ref.current.childNodes if(nodes.length === 0) return nodes.forEach((node: HTMLDivElement) => { if (!node) return; const rect = node.getBoundingClientRect(); // 獲取對應的元素信息 const index = +node.id; // 可以通過 id,來取到對應的索引 const oldHeight = state.positions[index].height // 舊的高度 const dHeight = oldHeight - rect.height // 差值 if(dHeight){ state.positions[index].height = rect.height // 真實高度 state.positions[index].bottom = state.positions[index].bottom - dHeight state.positions[index].dHeight = dHeight // 將差值保留 } });
// 重新計算整體的高度
const startId = +nodes[0].id
const positionLength = state.positions.length;
let startHeight = state.positions[startId].dHeight;
state.positions[startId].dHeight = 0;
for (let i = startId + 1; i < positionLength; ++i) {
const item = state.positions[i];
state.positions[i].top = state.positions[i - 1].bottom;
state.positions[i].bottom = state.positions[i].bottom - startHeight;
if (item.dHeight !== 0) {
startHeight += item.dHeight;
item.dHeight = 0;
}
}
// 重新計算子列表的高度
state.itemHeight = state.positions[positionLength - 1].bottom;
}
useCreation(() => { state.data = list.slice(state.start, state.end)
if(ref.current){
setPostition()
}
}, [state.end])
useEventListener('scroll', () => {
// 頂部高度
const { scrollTop, clientHeight, scrollHeight } = scrollRef.current
state.start = binarySearch(state.positions, scrollTop);
state.end = state.start + state.renderCount + 1
// 計算偏移量
state.currentOffset = state.start > 0 ? state.positions[state.start - 1].bottom : 0
// 滾動條距離的高度
const button = scrollHeight - clientHeight - scrollTop
if(button === 0 && onRequest){
onRequest()
}
}, scrollRef)
// 二分查找
const binarySearch = (list:any[], value: any) =>{ let start:number = 0; let end:number = list.length - 1; let tempIndex = null; while(start <= end){ let midIndex = parseInt(String( (start + end)/2)); let midValue = list[midIndex].bottom; if(midValue === value){ return midIndex + 1; }else if(midValue < value){ start = midIndex + 1; }else if(midValue > value){ if(tempIndex === null || tempIndex > midIndex){ tempIndex = midIndex; } end = end - 1; } } return tempIndex; }
return
{/* 佔位,列表的總高度,用於生成滾動條 _/}{/_ 內容區域 _/}_<div ref="{ref}" transform:="" _translate3d(0, ${state.currentOffset}px, 0), position: 'relative', left: 0, top: 0, right: 0}}> {/_ 渲染區域 _/} {state.data.map((item:any) =>_
_{/_ 子組件 */}
) }
}
export default HOC;
頁面代碼:
tsx
import React,{useEffect, useState} from 'react'; import { HOC } from '@/components'; import Mock from 'mockjs';
// 子組件 const Item:React.FC<{id: any, index?:number}> = ({id, index}) => {
return (
列表 {index}: {id}
) }
const ItemHoc = HOC(Item)
const Index:React.FC= (props)=> {
const [list, setList] = useState([])
useEffect(() => { let arr:any[] = [] for(let i = 0; i < 100; i++){ arr.push({ id: i, content: Mock.mock('@csentence(40, 100)') }) }
setList
(
arr
)
}, [])
if(list.length === 0) return <>
return (
); }
export default Index;
存在的問題
列表中可能存在由 圖片
撐起高度的情況,圖片會發送網絡請求,可能會造成計算不準確的問題,但這種情況比較少見,基本上可以忽略
End
本文是將兩種常見的 分片渲染
和 虛擬列表
的功能封裝成高階組件的形式,與傳統的懶加載還是有一定的區別,本質上有所不同
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/y1U6MLpW_STuxbcbIfPC9A