原生js虛擬滾動原理及示例

百度了一圈,好多都無法用,看了好多里面的原理,自己又加以改造


image.png
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Virtual Scroll List</title>
    <style>
      #scroll-container {
        position: relative;
        overflow-y: scroll;
        height: 600px;
        border: 1px solid blue;
      }
      #content {
        width: 100%;
      }
      .item {
        height: 200px;
        line-height: 200px;
        border: 1px solid #ccc;
        box-sizing: border-box;
      }
      #wrap {
        position: absolute;
        top: 0;
        left: 0;
        height: 600px;
        width: 100%;
      }
    </style>
  </head>
  <body>
    <!-- 顯示容器 -->
    <div id="scroll-container">
      <!-- 使顯示容器出現(xiàn)滾動條 -->
      <div id="content">
        <!-- 列表容器 -->
        <div id="wrap"></div>
      </div>
    </div>
    <script>
      const data = Array.from({ length: 1000 }, (_, i) => `Item ${i}`);
      const wrapHeight = 600;
      const itemHeight = 200;
      const container = document.getElementById("scroll-container");
      // 使container元素出現(xiàn)滾動條
      const content = document.getElementById("content");
      content.style.height = itemHeight * data.length + "px";
      // 列表元素容器
      const wrap = document.getElementById("wrap");

      function renderItems(startIndex, endIndex, offset, scrollTop) {
        wrap.innerHTML = "";
        console.log(startIndex, endIndex, offset);

        for (let i = startIndex; i < endIndex; i++) {
          const item = document.createElement("div");
          item.className = "item";
          item.textContent = data[i];
          wrap.appendChild(item);
        }
        // 計算最大滾動距離,超過后鼠標(biāo)滾輪滾動后不再滾動
        const maxScrollTop = data.length * itemHeight - wrapHeight;
        if (scrollTop >= maxScrollTop) {
          // 調(diào)整列表容器位置
          wrap.style.top = maxScrollTop - offset + "px";
          // 讓滾動條移動
          container.scrollTop = maxScrollTop;
        } else {
          // 調(diào)整列表容器位置和滾動條位置
          wrap.style.top = scrollTop - offset + "px";
          container.scrollTop = scrollTop;
        }
      }

      function onScroll() {
        const scrollTop = container.scrollTop;
        console.log("scrolltop", scrollTop);

        const start = scrollTop / itemHeight;
        // 開始展示的項目索引
        const startIndex = Math.floor(scrollTop / itemHeight);
        // 結(jié)束的項目索引
        const endIndex = Math.ceil((scrollTop + container.clientHeight) / itemHeight);
        // 是否剛好展示第一個完整項
        const isZero = scrollTop % itemHeight === 0;
        // 不是完整項則計算偏移量
        const offset = isZero ? 0 : itemHeight - (Math.ceil(scrollTop / itemHeight) * itemHeight - scrollTop);
        renderItems(startIndex, endIndex, offset, scrollTop);
      }

      container.addEventListener("scroll", onScroll);
      // 初始渲染
      onScroll();
    </script>
  </body>
</html>

vue虛擬滾動組件
item.vue

<template>
  <!-- 顯示容器 -->
  <div id="scroll-container" ref="scroll-container" @scroll="onScroll">
    <!-- 使顯示容器出現(xiàn)滾動條 -->
    <div
      id="content"
      ref="content"
      :style="{ height: itemHeight * list.length + 'px' }"
    >
      <!-- 列表容器 -->
      <div id="wrap" ref="wrap"></div>
    </div>
  </div>
</template>
 
<script>
export default {
  name: "MyVirtualList",
  props: {
    list: {
      type: Array,
      default: () => [],
    },
    itemHeight: {
      type: Number,
      default: 200,
    },
    wrapHeight: {
      type: Number,
      default: 600,
    },
  },

  data() {
    return {};
  },
  mounted() {
    // const data = Array.from({ length: 1000 }, (_, i) => `Item ${i}`);
    // 初始渲染
    this.onScroll();
  },
  computed: {},
  methods: {
    onScroll() {
      const scrollTop = this.$refs["scroll-container"].scrollTop;
      console.log("scrolltop", scrollTop);

      const start = scrollTop / this.itemHeight;
      // 開始展示的項目索引
      const startIndex = Math.floor(scrollTop / this.itemHeight);
      // 結(jié)束的項目索引
      const endIndex = Math.ceil(
        (scrollTop + this.$refs["scroll-container"].clientHeight) /
          this.itemHeight
      );
      // 是否剛好展示第一個完整項
      const isZero = scrollTop % this.itemHeight === 0;
      // 不是完整項則計算偏移量
      const offset = isZero
        ? 0
        : this.itemHeight -
          (Math.ceil(scrollTop / this.itemHeight) * this.itemHeight -
            scrollTop);
      this.renderItems(startIndex, endIndex, offset, scrollTop);
    },
    renderItems(startIndex, endIndex, offset, scrollTop) {
      this.$refs.wrap.innerHTML = "";
      console.log(startIndex, endIndex, offset);

      for (let i = startIndex; i < endIndex; i++) {
        const item = document.createElement("div");
        item.className = "item";
        item.textContent = this.list[i].label;
        this.$refs.wrap.appendChild(item);
      }
      // 計算最大滾動距離,超過后鼠標(biāo)滾輪滾動后不再滾動
      const maxScrollTop = this.list.length * this.itemHeight - this.wrapHeight;
      if (scrollTop >= maxScrollTop) {
        // 調(diào)整列表容器位置
        this.$refs.wrap.style.top = maxScrollTop + "px";
        // 讓滾動條移動
        this.$refs["scroll-container"].scrollTop = maxScrollTop;
      } else {
        // 調(diào)整列表容器位置和滾動條位置
        this.$refs.wrap.style.top = scrollTop - offset + "px";
        this.$refs["scroll-container"].scrollTop = scrollTop;
      }
    },
  },
};
</script>
 
<style scoped>
#scroll-container {
  position: relative;
  overflow-y: scroll;
  height: 600px;
  border: 1px solid blue;
}
#content {
  width: 100%;
}
::v-deep .item {
  height: 200px;
  line-height: 200px;
  border: 1px solid #ccc;
  box-sizing: border-box;
}
#wrap {
  position: absolute;
  top: 0;
  left: 0;
  height: 600px;
  width: 100%;
}
</style>

list.vue

<template>
  <div class="container">
    <my-virtual-scroller :list="list" :itemHeight="200" :wrapHeight="600" />
  </div>
</template>

<script>
// 模擬一個長列表
const list = [];
for (let i = 0; i < 10000; i++) {
  list.push({
    id: i,
    label: `virtual-list ${i}`,
  });
}
export default {
  components: {
    myVirtualScroller,
  },
  data() {
    return {
      list: list,
    };
  },
};
</script>

<style scoped>
.container {
  height: 600px;
  border: 1px solid #ccc;
}
</style>

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

推薦閱讀更多精彩內(nèi)容