花三個小時,完全掌握分片渲染和虛擬列表~

大家好,我是 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;

效果:

就能實現這樣的效果,這個主要有以下兩個缺點:

進行改造

分析:

  1. 針對上述的第一點,我們可以把裏面的數組包裝成一個整體,通過 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 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

渲染區域的計算點:其實我們渲染的數據只是可視區緩衝區,我們可以利用 slicelist進行截取,所以在我們還需要知道:

渲染節點的數量 = 容器的高度 / 子列表高度 (需要向上取整) + 緩衝個數

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,數據的變化是由滾動事件所引起的,造成 startend的改變,所以在這裏,我們可以使用 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來模擬些數據看看:

思考

對於子列表的動態高度我們該如何處理?

  1. 第一種,將 ItemHeight作爲參數傳遞過來,我們可以根據傳遞 數組來控制,但這種情況需要我們提前將列表的高度算出來,算每個子列表的高度很麻煩,其次這個高度還要根據屏幕的大小去變化,這個方法明顯不適合

  2. 第二種, 預算高度,我們可以假定子列表的高度也就是虛假高度( initItemHeight), 當我們渲染的時候,在更新對應高度,這樣就可以解決子列表高度的問題

預算高度該如何考慮

針對第二種方案,我們需要去維護一個公共的高度列表( positions),這個數組將會記錄真實的 DOM 高度

那麼 positions需要記錄那些信息:

 const state = useReactive<any>({
    ...,
    positions: [ //需要記錄每一項的高度
      // index         // 當前pos對應的元素的下標
      // top;          // 頂部位置
      // bottom        // 底部位置
      // height        // 元素高度
      // dHeight        // 用於判斷是否需要改變
    ], 
    initItemHeight: 50, // 預計高度
  })

需要記錄 元素的高度,其次可以存入距離頂部和底部的高度,方便後面計算偏移量和列表的整體高度,在設定一個參數( dHeight)判斷新的高度與舊的高度是否一樣,不一樣的話就進行更新

其中最重要的就是 index,它用來記錄子列表真實高度的下標,這個點極爲重要,原因是:在之前的講解中,我們發現 startend的差值實際上是不變的,也就是說,最終渲染的數據,實際上是一個 固定值,但裏面的子列表高度卻是 變值, 所以我們需要有一個變量來區分數據所對應的高度,所以這個 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;
  }

這樣就可以將真實的高度替換虛擬的高度 除了首次的渲染之外,還有就是在startend改變時重新計算,也就是

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