如何制作虚拟卷轴?
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 像素
- 此视口有一个称为根的父容器,它只显示所有内容的子集
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"
});
请教我如何制作虚拟卷轴。我使用 HTML、JS、Vue。我尝试过使用vue-virtual-scroll,但是由于很难改成我想要的功能,所以我打算做一个基础部分并应用它。请告诉我如何制作基本的虚拟卷轴。
虽然在您的问题的评论中提到了概念和参考资料,但这是我在 Vue.js
中对简单虚拟滚动器的实现- 在各处添加了注释,因此代码很容易解释 支持固定项目高度
- 我们的想法是在一个名为 spacer 的 div 中显示一个项目列表
- 这个 spacer 有一个容器 div,它不断地垂直移动,称为视口
- 如果每个项目的高度为 30 像素,并且您想显示项目 4 到 20,同时隐藏项目 0、项目 1、项目 2 和项目 3,则此视口将垂直平移 120 像素
- 此视口有一个称为根的父容器,它只显示所有内容的子集
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"
});