虛擬列表

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    .list-container {
      overflow: auto;
      border: 1px solid black;
      height: 500px;
    }
  </style>

  <body>
    <!-- 外部容器用來固定列表容器的高度,同時生成滾動條 -->
    <div class="list-container">
      <!-- 內部容器用來裝元素,高度是所有元素高度的和 -->
      <div class="list-container-inner"></div>
    </div>

    <script>
      /** --------- 一些基本變量 -------- */
      const itemHeight = 60;
      const height = 500;

      /** --------- 生成數據 -------- */
      const getRandomHeight = () => {
        // 返回 [60, 150] 之間的隨機數
        return Math.floor(Math.random() * (150 - itemHeight + 1) + itemHeight);
      };
      const initData = () => {
        const data = [];
        for (let i = 0; i < 15; i++) {
          data.push({
            content: `內容:${i}`,
            height: getRandomHeight(),
            color: i % 2 ? 'red' : 'yellow',
          });
        }
        return data;
      };
      const data = initData();
      console.log('data:', data);
      const cacheHeightMap = {};
      const outerContainer = document.querySelector('.list-container');

      const scrollCallback = () => {
        let contentHeight = 0;
        let paddingTop = 0;
        let upperHeight = 0;
        let startIndex;
        let endIndex;
        const innerContainer = document.querySelector('.list-container-inner');
        const scrollTop = Math.max(outerContainer.scrollTop, 0);

        // 遍歷所有的元素,獲取當前元素的高度、列表總高度、startIndex、endIndex
        for (let i = 0; i < data.length; i++) {
          // 初始化的時候因為元素還沒有渲染,無法獲取元素的高度
          // 所以用元素的最小高度itemHeight來進行計算,保證渲染的元素個數能占滿列表
          const cacheHeight = cacheHeightMap[i];
          const usedHeight =
            cacheHeight === undefined ? itemHeight : cacheHeight;

          contentHeight += usedHeight;

          if (contentHeight >= scrollTop && startIndex === undefined) {
            startIndex = i;
            paddingTop = contentHeight - usedHeight;
          }

          if (contentHeight > scrollTop + height && endIndex === undefined) {
            endIndex = i;
            upperHeight = contentHeight;
          }
        }

        // 應對列表所有元素沒有占滿整個容器的情況
        if (endIndex === undefined) {
          endIndex = data.length - 1;
          upperHeight = contentHeight;
        }

        // 未渲染的元素的高度由padding-top和padding-bottom代替,保證滾動條位置正確
        // 這里如果把設置pading的操作放在渲染元素之后,部分瀏覽器滾動到最后一個元素時會有問題
        const paddingBottom = contentHeight - upperHeight;

        innerContainer.setAttribute(
          'style',
          `padding-top: ${paddingTop}px; padding-bottom: ${paddingBottom}px`
        );

        // 從data取出要渲染的元素并渲染到容器中
        const viewData = data.slice(startIndex, endIndex + 1);

        innerContainer.innerHTML = '';

        const fragment = document.createDocumentFragment();

        for (let i = 0; i < viewData.length; i++) {
          const item = document.createElement('div');
          const itemData = viewData[i];

          item.innerHTML = itemData.content;
          item.setAttribute(
            'style',
            `height: ${itemData.height}px; background: ${itemData.color}`
          );

          fragment.appendChild(item);
        }

        innerContainer.appendChild(fragment);

        // 存儲已經渲染出來的元素的高度,供后面使用
        const children = innerContainer.children;

        let flag = startIndex;

        for (const child of children) {
          cacheHeightMap[flag] = child.offsetHeight;
          flag++;
        }
      };

      // 首屏渲染
      scrollCallback();
      // 監聽外部容器的滾動事件
      outerContainer.addEventListener('scroll', scrollCallback);
    </script>
  </body>
</html>

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容