大家都能看得懂的源碼之 ahooks useVirtualList 封裝虛擬滾動列表

来源:https://www.cnblogs.com/gopal/archive/2022/09/07/16667118.html
-Advertisement-
Play Games

本文是深入淺出 ahooks 源碼系列文章的第十八篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。 簡介 提供虛擬化列表能力的 Hook,用於解決展示海量數據渲染時首屏渲染緩慢和滾動卡頓問題。 詳情可見官網,文章源代碼可以點擊這裡。 實現原理 其實現原理監聽外部容 ...


本文是深入淺出 ahooks 源碼系列文章的第十八篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。

簡介

提供虛擬化列表能力的 Hook,用於解決展示海量數據渲染時首屏渲染緩慢和滾動卡頓問題。

詳情可見官網,文章源代碼可以點擊這裡

實現原理

其實現原理監聽外部容器的 scroll 事件以及其 size 發生變化的時候,觸發計算邏輯算出內部容器的高度和 marginTop 值。

具體實現

其監聽滾動邏輯如下:

// 當外部容器的 size 發生變化的時候,觸發計算邏輯
useEffect(() => {
  if (!size?.width || !size?.height) {
    return;
  }
  // 重新計算邏輯
  calculateRange();
}, [size?.width, size?.height, list]);

// 監聽外部容器的 scroll 事件
useEventListener(
  'scroll',
  e => {
    // 如果是直接跳轉,則不需要重新計算
    if (scrollTriggerByScrollToFunc.current) {
      scrollTriggerByScrollToFunc.current = false;
      return;
    }
    e.preventDefault();
    // 計算
    calculateRange();
  },
  {
    // 外部容器
    target: containerTarget,
  },
);

其中 calculateRange 非常重要,它基本實現了虛擬滾動的主流程邏輯,其主要做了以下的事情:

  • 獲取到整個內部容器的高度 totalHeight。
  • 根據外部容器的 scrollTop 算出已經“滾過”多少項,值為 offset。
  • 根據外部容器高度以及當前的開始索引,獲取到外部容器能承載的個數 visibleCount。
  • 並根據 overscan(視區上、下額外展示的 DOM 節點數量)計算出開始索引(start)和(end)。
  • 根據開始索引獲取到其距離最開始的距離(offsetTop)。
  • 最後根據 offsetTop 和 totalHeight 設置內部容器的高度和 marginTop 值。

變數很多,可以結合下圖,會比較清晰理解:

image

代碼如下:

// 計算範圍,由哪個開始,哪個結束
const calculateRange = () => {
  // 獲取外部和內部容器
  // 外部容器
  const container = getTargetElement(containerTarget);
  // 內部容器
  const wrapper = getTargetElement(wrapperTarget);

  if (container && wrapper) {
    const {
      // 滾動距離頂部的距離。設置或獲取位於對象最頂端和視窗中可見內容的最頂端之間的距離
      scrollTop,
      // 內容可視區域的高度
      clientHeight,
    } = container;

    // 根據外部容器的 scrollTop 算出已經“滾過”多少項
    const offset = getOffset(scrollTop);
    // 可視區域的 DOM 個數
    const visibleCount = getVisibleCount(clientHeight, offset);

    // 開始的下標
    const start = Math.max(0, offset - overscan);
    // 結束的下標
    const end = Math.min(list.length, offset + visibleCount + overscan);

    // 獲取上方高度
    const offsetTop = getDistanceTop(start);
    // 設置內部容器的高度,總的高度 - 上方高度
    // @ts-ignore
    wrapper.style.height = totalHeight - offsetTop + 'px';
    // margin top 為上方高度
    // @ts-ignore
    wrapper.style.marginTop = offsetTop + 'px';
    // 設置最後顯示的 List
    setTargetList(
      list.slice(start, end).map((ele, index) => ({
        data: ele,
        index: index + start,
      })),
    );
  }
};

其它就是這個函數的輔助函數了,包括:

  • 根據外部容器以及內部每一項的高度,計算出可視區域內的數量:
// 根據外部容器以及內部每一項的高度,計算出可視區域內的數量
const getVisibleCount = (containerHeight: number, fromIndex: number) => {
  // 知道每一行的高度 - number 類型,則根據容器計算
  if (isNumber(itemHeightRef.current)) {
    return Math.ceil(containerHeight / itemHeightRef.current);
  }

  // 動態指定每個元素的高度情況
  let sum = 0;
  let endIndex = 0;
  for (let i = fromIndex; i < list.length; i++) {
    // 計算每一個 Item 的高度
    const height = itemHeightRef.current(i, list[i]);
    sum += height;
    endIndex = i;
    // 大於容器寬度的時候,停止
    if (sum >= containerHeight) {
      break;
    }
  }
  // 最後一個的下標減去開始一個的下標
  return endIndex - fromIndex;
};
  • 根據 scrollTop 計算上面有多少個 DOM 節點:
// 根據 scrollTop 計算上面有多少個 DOM 節點
const getOffset = (scrollTop: number) => {
  // 每一項固定高度
  if (isNumber(itemHeightRef.current)) {
    return Math.floor(scrollTop / itemHeightRef.current) + 1;
  }
  // 動態指定每個元素的高度情況
  let sum = 0;
  let offset = 0;
  // 從 0 開始
  for (let i = 0; i < list.length; i++) {
    const height = itemHeightRef.current(i, list[i]);
    sum += height;
    if (sum >= scrollTop) {
      offset = i;
      break;
    }
  }
  // 滿足要求的最後一個 + 1
  return offset + 1;
};
  • 獲取上部高度:
// 獲取上部高度
const getDistanceTop = (index: number) => {
  // 每一項高度相同
  if (isNumber(itemHeightRef.current)) {
    const height = index * itemHeightRef.current;
    return height;
  }
  // 動態指定每個元素的高度情況,則 itemHeightRef.current 為函數
  const height = list
    .slice(0, index)
    // reduce 計算總和
    // @ts-ignore
    .reduce((sum, _, i) => sum + itemHeightRef.current(i, list[index]), 0);
  return height;
};
  • 計算總的高度:
// 計算總的高度
const totalHeight = useMemo(() => {
  // 每一項高度相同
  if (isNumber(itemHeightRef.current)) {
    return list.length * itemHeightRef.current;
  }
  // 動態指定每個元素的高度情況
  // @ts-ignore
  return list.reduce(
    (sum, _, index) => sum + itemHeightRef.current(index, list[index]),
    0,
  );
}, [list]);

最後暴露一個滾動到指定的 index 的函數,其主要是計算出該 index 距離頂部的高度 scrollTop,設置給外部容器。並觸發 calculateRange 函數。

// 滾動到指定的 index
const scrollTo = (index: number) => {
  const container = getTargetElement(containerTarget);
  if (container) {
    scrollTriggerByScrollToFunc.current = true;
    // 滾動
    container.scrollTop = getDistanceTop(index);
    calculateRange();
  }
};

思考與總結

對於高度相對比較確定的情況,我們做虛擬滾動還是相對簡單的,但假如高度不確定呢?

或者換另外一個角度,當我們的滾動不是縱向的時候,而是橫向,該如何處理呢?

本文已收錄到個人博客中,歡迎關註~


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 摘要:北京國家金融科技認證中心正式公佈了2022年通過“分散式資料庫金融標準驗證”的資料庫產品名單。華為雲GaussDB金融級分散式資料庫以突出的技術優勢通過驗證,躍然榜上,且測試得分遙居前列。 近日,北京國家金融科技認證中心正式公佈了2022年通過“分散式資料庫金融標準驗證”的資料庫產品名單。華為 ...
  • 在2022世界人工智慧大會(WAIC)上,騰訊雲資料庫技術負責人程彬為大家分享了資料庫與 AI 相結合背後的故事。在專場《當資料庫遇上 AI 》中,程彬基於騰訊雲資料庫在 AI 智能化的探索與實踐,剖析資料庫與 AI 融合背後的技術關鍵點,為產業界提供前沿解決方案。以下為演講實錄: 點擊觀看完整版直 ...
  • 項目管理構建工具——Maven(基礎篇) 在前面的內容中我們學習了JDBC並且接觸到了jar包概念 在後面我們的實際開發中會接觸到很多jar包,jar包的導入需要到互聯網上進行就會導致操作繁瑣 Maven在解決了jar包導入繁雜問題的同時,也提供了一套通用的管理和構建Java項目的一系列操作 Mav ...
  • 近年來隨著物聯網技術以及農業自動化應用水平的不斷發展,基於“互聯網+”項目經驗日漸豐富,通過感測器採集對應的信息再通過一些組態軟體類實現自動運轉、自動控制的智能大棚勢在必得。 ...
  • 上傳IPA到iTunes Connect 上一篇我介紹瞭如何在iTunes Connect里準備應用。最後在這篇文章里我會簡單介紹下如何來上傳IPA到iTunes Connect。 登陸iTunes Connect,進入Manage Your Applications頁面後,點擊你創建的應用圖標,進 ...
  • DanceCC提出了一套專門的方案。方案原理基於LLDB Plugin,利用Fishhook,從LLDB的Script Bridge API層面攔截Xcode對LLDB調用,以此來進行耗時監控統計。 ...
  • HMS Core應用內支付服務(In-App Purchases,IAP)為應用提供便捷的應用內支付體驗和簡便的接入流程。開發者的應用集成IAP SDK後,調用IAP SDK介面,啟動IAP收銀台,即可實現應用內支付。 通過應用內支付服務,用戶可以在應用內購買各種類型的虛擬商品,包括一次性商品(包括 ...
  • 最近有個需求,就是上傳圖片的時候,圖片過大,需要壓縮一下圖片再上傳。 需求雖然很容易理解,但要做到,不是那麼容易的。 這裡涉及到的知識有點多,不多說,本篇博客有點重要呀! 一、圖片URL轉Blob(圖片大小不變) 註意點:圖片不能跨域!!! 方式一:通過XHR請求獲取 function urlToB ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...