如何制作虚拟卷轴?

How to make virtual scroll?

请教我如何制作虚拟卷轴。我使用 HTML、JS、Vue。我尝试过使用vue-virtual-scroll,但是由于很难改成我想要的功能,所以我打算做一个基础部分并应用它。请告诉我如何制作基本的虚拟卷轴。

虽然在您的问题的评论中提到了概念和参考资料,但这是我在 Vue.js

中对简单虚拟滚动器的实现
  • 在各处添加了注释,因此代码很容易解释 支持固定项目高度
  • 我们的想法是在一个名为 spacer
  • 的 div 中显示一个项目列表
  • 这个 spacer 有一个容器 div,它不断地垂直移动,称为视口
  • 如果每个项目的高度为 30 像素,并且您想显示项目 4 到 20,同时隐藏项目 0、项目 1、项目 2 和项目 3,则此视口将垂直平移 120 像素
  • 此视口有一个称为根的父容器,它只显示所有内容的子集

WORKING VERSION HERE

HTML

<!-- https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib -->

<script type="text/x-template" id="virtual-scroll">
  <div class="root" ref="root" :style="rootStyle">
    <div class="viewport" ref="viewport" :style="viewportStyle">
      <div class="spacer" ref="spacer" :style="spacerStyle">
        <div v-for="item in visibleItems" :key="item">
          {{item}}
        </div>
      </div>
    </div>
  </div>
</script>

<div id="app">
  <header>
    <h1>Vue.js Virtual Scroller</h1>
    <h2>No Libraries Used</h2>
    <h3>Keep Only a few items in DOM for a very large list</h3>
    <p>Scroll below either by dragging the scroll bar or by moving your mouse wheel. Right Click any item in the list, click <b>Inspect Element</b> and check out the number of items in DOM, it is constant! Do you see how <b>smooth</b> it scrolls? Feel free to play with the number of items </p>
  </header>
  <virtual-scroll></virtual-scroll>
</div>

CSS

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html {
  height: 100%;
}

body {
  min-height: 100%;
  height: 100%;
  font-family: "Noto Sans", "Tahoma", sans-serif;
  display: flex;
  flex-direction: column;
  color: rgba(0,0,0,0.6);
  padding: 1.25rem;
}

header {
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 0 1rem;
}

#app {
  height: 100%;
}

.viewport {
  background: #fefefe;
  overflow-y: auto;
}

.spacer > div {
  padding: 0.5rem 0rem;
  border: 1px solid #f5f5f5;
}

Vue.js

// https://dev.to/adamklein/build-your-own-virtual-scroll-part-i-11ib
// define a mixin object
var passiveSupportMixin = {
  methods: {
    // This snippet is taken straight from https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
    // It will only work on browser so if you are using in an SSR environment, keep your eyes open
    doesBrowserSupportPassiveScroll() {
      let passiveSupported = false;

      try {
        const options = {
          get passive() {
            // This function will be called when the browser
            //   attempts to access the passive property.
            passiveSupported = true;
            return false;
          }
        };

        window.addEventListener("test", null, options);
        window.removeEventListener("test", null, options);
      } catch (err) {
        passiveSupported = false;
      }
      return passiveSupported;
    }
  }
};

Vue.component("VirtualScroll", {
  template: "#virtual-scroll",
  mixins: [passiveSupportMixin],
  data() {
    return {
      // A bunch of items with numbers from 1 to N, should be a props ideally
      items: new Array(10000)
        .fill(null)
        .map((item, index) => "Item " + (index + 1)),
      // Total height of the root which contains all the list items in px
      rootHeight: 400,
      // Height of each row, give it an initial value but this gets calculated dynamically on mounted
      rowHeight: 30,
      // Current scroll top position, we update this inside the scroll event handler
      scrollTop: 0,
      // Extra padding at the top and bottom so that the items transition smoothly
      // Think of it as extra items just before the viewport starts and just after the viewport ends
      nodePadding: 20
    };
  },
  computed: {
    /**
    Total height of the viewport = number of items in the array x height of each item
    */
    viewportHeight() {
      return this.itemCount * this.rowHeight;
    },
    /**
    Out of all the items in the massive array, we only render a subset of them
    This is the starting index from which we show a few items
    */
    startIndex() {
      let startNode =
        Math.floor(this.scrollTop / this.rowHeight) - this.nodePadding;
      startNode = Math.max(0, startNode);
      return startNode;
    },
    /**
    This is the number of items we show after the starting index
    If the array has a total 10000 items, we want to show items from say index 1049 till 1069
    visible node count is that number 20 and starting index is 1049
    */
    visibleNodeCount() {
      let count =
        Math.ceil(this.rootHeight / this.rowHeight) + 2 * this.nodePadding;
      count = Math.min(this.itemCount - this.startIndex, count);
      return count;
    },
    /**
    Subset of items shown from the full array
    */
    visibleItems() {
      return this.items.slice(
        this.startIndex,
        this.startIndex + this.visibleNodeCount
      );
    },
    itemCount() {
      return this.items.length;
    },
    /**
    The amount by which we need to translateY the items shown on the screen so that the scrollbar shows up correctly
    */
    offsetY() {
      return this.startIndex * this.rowHeight;
    },
    /**
    This is the direct list container, we apply a translateY to this
    */
    spacerStyle() {
      return {
        transform: "translateY(" + this.offsetY + "px)"
      };
    },
    viewportStyle() {
      return {
        overflow: "hidden",
        height: this.viewportHeight + "px",
        position: "relative"
      };
    },
    rootStyle() {
      return {
        height: this.rootHeight + "px",
        overflow: "auto"
      };
    }
  },
  methods: {
    handleScroll(event) {
      this.scrollTop = this.$refs.root.scrollTop;
    },
    /**
    Find the largest height amongst all the children
    Remember each row has to be of the same height
    I am working on the different height version
    */
    calculateInitialRowHeight() {
      const children = this.$refs.spacer.children;
      let largestHeight = 0;
      for (let i = 0; i < children.length; i++) {
        if (children[i].offsetHeight > largestHeight) {
          largestHeight = children[i].offsetHeight;
        }
      }
      return largestHeight;
    }
  },
  mounted() {
    this.$refs.root.addEventListener(
      "scroll",
      this.handleScroll,
      this.doesBrowserSupportPassiveScroll() ? { passive: true } : false
    );
    // Calculate that initial row height dynamically
    const largestHeight = this.calculateInitialRowHeight();
    this.rowHeight =
      typeof largestHeight !== "undefined" && largestHeight !== null
        ? largestHeight
        : 30;
  },
  destroyed() {
    this.$refs.root.removeEventListener("scroll", this.handleScroll);
  }
});

new Vue({
  el: "#app"
});