虚拟滚动 实现

虚拟滚动 实现


javascript 性能优化 虚拟滚动 前端
<!DOCTYPE html>
<html>
  <head>
    <title>TestVirtualScroll</title>
    <script src="https://unpkg.com/vue@next"></script>
  </head>
  <body>
    <div id="counter">
      <div class="outer" @scroll="onScroll">
        <ul :style="{height:totalHeight}">
          <li
            v-for="(item,index) in renderDisplayContent()"
            :style="rowStyle(item.orderNum)"
          >
            row:{{item.data.name}}{{item.orderNum}}
          </li>
        </ul>
      </div>
    </div>
    <script type="text/javascript">
      const Counter = {
        data() {
          return {
            counter: 0,
            wrapperHeight: 800,
            rowHeight: 40,
            startIndex: 0,
            endIndex: 0,
            content: [],
          };
        },
        computed: {
          total() {
            return this.content.length;
          },
          totalHeight() {
            return this.total * this.rowHeight + "px";
          },
          limit() {
            return Math.min(
              this.total,
              Math.ceil(this.wrapperHeight / this.rowHeight)
            );
          },
        },
        methods: {
          onScroll(evt) {
            const { scrollTop } = evt.target;
            const { startIndex, total, rowHeight, limit } = this;

            const currentStartIndex = Math.floor(scrollTop / rowHeight);

            // 如果currentStartIndex 和 startIndex 不同(我们需要更新数据了)
            if (currentStartIndex !== startIndex) {
              this.startIndex = currentStartIndex;
              this.endIndex = Math.min(currentStartIndex + limit, total - 1);
            }
          },
          renderDisplayContent() {
            if (this.content && this.content.length) {
              const { rowHeight, startIndex, endIndex } = this;
              const content = [];
              // 用了 <= 是为了渲染x+1个元素用来在让滚动变得连续
              for (let i = startIndex; i <= endIndex; ++i) {
                content.push({ orderNum: i, data: this.content[i] || {} });
              }
              return content;
            } else {
              return [];
            }
          },
          //每行数据使用绝对定位展示
          rowStyle(index) {
            return {
              width: "100%",
              height: this.rowHeight + "px",
              lineHeight: this.rowHeight + "px",
              position: "absolute",
              left: 0,
              right: 0,
              top: index * this.rowHeight + "px",
              borderBottom: "1px solid #000",
            };
          },
        },
        mounted() {
          setTimeout(
            function () {
              this.content.length = 200000;
              this.content.fill({ name: "Niko" });
              this.endIndex = this.limit - 1;
            }.bind(this),
            500
          );
          //初始化时 endIndex赋值为最大展示条数
        },
      };

      Vue.createApp(Counter).mount("#counter");
    </script>
  </body>
  <style type="text/css">
    .outer {
      border: 1px solid;
      height: 300px;
      width: 200px;
      overflow: auto;
    }
    .outer ul {
      position: relative;
    }
    .outer li {
      height: 20px;
      line-height: 20px;
    }
  </style>
</html>
© 2025 Niko Xie