vue實現(xiàn)虛擬滾動的示例詳解
虛擬滾動或者移動是指禁止原生滾動,之后通過監(jiān)聽瀏覽器的相關事件實現(xiàn)模擬滾動。所以虛擬滾動包含兩部分內容
1.禁止原生滾動:將css的overfow屬性設置為hidden。這樣即便是內容高度或者寬度超過了盒子的寬度或者高度也無法進行滾動了
<div id="vs-container">
<div id="vs-content">
<p>內容</p>
<p>內容</p>
<p>內容</p>
<p>內容</p>
<p>內容</p>
<p>內容</p>
<p>內容</p>
<p>內容</p>
</div>
</div>
<style>
#vs-container {
overflow:hidden;
height:100px;
}
#vs-content {
height:200px;
}
</style>
2.模擬滾動:通過監(jiān)聽鼠標的wheel事件,調整內容位置,從而形成滾動效果;通過監(jiān)聽onmousedown、onmousemove、onmouseup實現(xiàn)虛擬滾動條的移動
解決什么問題
- 服務虛擬列表,尤其不定高度內容的虛擬列表實現(xiàn);不定高內容虛擬列表在滑動過程中由于滾動速度大于渲染速度導致過快滑動時出現(xiàn)白屏現(xiàn)象。如果有虛擬滾動,則可以先進行數(shù)據(jù)渲染待渲染完畢再進行滾動,這樣就徹底解決了白屏問題。
- 在我工作中遇到使用虛擬列表實現(xiàn)不定高數(shù)據(jù)渲染問題,正好也出現(xiàn)了白屏問題
Dom結構
本文使用vue2實現(xiàn)虛擬滾動,DOM結構以及一些初始化數(shù)據(jù)如下
內容和盒子
<template>
<div id="vs-container" ref="container">
<div id="vs-content" :style="{ transform: contentTransform }">
<p :key="num" v-for="num in list">{{ num }}</p>
</div>
</div>
</template>
<script>
export default {
data () {
return {
list: 1000,
contentOffset: 0
}
},
computed: {
contentTransform () {
return `translate3d(${this.contentOffset}px)`
}
}
}
</script>
<style lang="scss" scoped>
#vs-container {
margin-top: 200px;
margin-left: 20px;
height: 200px;
border: 1px solid #333;
overflow: hidden;
width: 500px;
position: relative;
box-sizing: border-box;
}
</style>上述代碼內容id為vs-content,盒子id為vs-container,盒子高度200px,并且禁止盒子的原生滾動,設置盒子overflow為hidden。contentTransform用來動態(tài)變化滾動位置。給盒子增加ref,標記container為后面開發(fā)使用。
虛擬滾動條
在上述代碼中添加虛擬滾動條,虛擬滾動條包括滑道,其ref設置為slider;還包括手柄,手柄ref為handle
<template>
<div id="vs-container" ref="container">
<div id="vs-content" :style="{ transform: contentTransform }">
<p :key="num" v-for="num in list">{{ num }}</p>
</div>
<div id="vs-slider" ref="slider">
<div
id="vs-handle"
:style="{ transform: handleTransformt }"
ref="handle"
></div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
...
handleOffset: 0
}
},
computed: {
...
handleTransform () {
return `translateY(${this.handleOffset}px)`
}
}
}
</script>
<style lang="scss" scoped>
#vs-container {
...
#vs-slider {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 10px;
height:20px;
box-sizing: border-box;
background-color: #6b6b6b;
#vs-handle {
background-color: #f1f2f3;
cursor: pointer;
border-radius: 10px;
}
}
}
</style>contentTransform用來動態(tài)變化虛擬滾動條的滾動位置,設置滾動條高度20px。到此處整個虛擬滾動示例長這樣

虛擬滾動實現(xiàn)
實現(xiàn)虛擬滾動,開頭說了模擬滾動原理:通過監(jiān)聽鼠標的wheel事件,調整內容位置,從而形成滾動效果;通過監(jiān)聽onmousedown、onmousemove、onmouseup實現(xiàn)虛擬滾動條的移動。
本文使用translateY值的變化實現(xiàn)內容區(qū)或虛擬滾動條的滾動。本文只實現(xiàn)垂直方向上的滾動,水平方向上的滾動原理基本一致。
監(jiān)聽鼠標滾輪或觸屏版實現(xiàn)內容區(qū)滾動
使用上文中ref獲取相應的dom元素,然后給內容區(qū)盒子container綁定wheel事件。
監(jiān)聽wheel事件獲取事件對象的wheelDeltaY,其含義為
返回一個整型數(shù),表示垂直滾動量。
在谷歌瀏覽器下,如果是觸屏版滑動返回0、1、2、3……或者0、-1、-2、-3……,如果是鼠標滾輪滾動返回150或-150。具體實現(xiàn)內容區(qū)滾動
<template>
<div id="vs-container" ref="container">
<div id="vs-content" :style="{ transform: contentTransform }">
<p :key="num" v-for="num in list">{{ num }}</p>
</div>
<div id="vs-slider" ref="slider">
<div
id="vs-handle"
:style="{ transform: handleTransform, height: handleStyleHeight }"
ref="handle"
></div>
</div>
</div>
</template>
<script>
export default {
methods: {
bindContainerEvent () {
const { $container } = this.$element
const contentSpace = $container.scrollHeight - $container.offsetHeight
const bindContainerOffset = (event) => {
event.preventDefault()
this.contentOffset += event.wheelDeltaY
if (this.contentOffset < 0) {
this.contentOffset = Math.max(this.contentOffset, -contentSpace)
} else {
this.contentOffset = 0
}
}
$container.addEventListener('wheel', bindContainerOffset)
this.unbindContainerEvent = () => {
$container.removeEventListener('wheel', bindContainerOffset)
}
},
// 獲取dom元素
saveHtmlElementById () {
const { container, slider, handle } = this.$refs
this.$element = {
$container: container,
$slider: slider,
$handle: handle
}
this.bindContainerEvent()
}
},
created () {
this.$nextTick(() => {
this.saveHtmlElementById()
})
},
beforeDestroy () {
this.unbindContainerEvent()
}
}
</script>event.wheelDeltaY值為負值,表示內容區(qū)向上滾動,反之內容區(qū)向下滾動。之后需要限制滾動區(qū)間
if (this.contentOffset < 0) {
this.contentOffset = Math.max(this.contentOffset, -contentSpace)
} else {
this.contentOffset = 0
}
內容區(qū)向上移動的最大距離為contentSpace,向下滾動的最大距離為0。
監(jiān)聽虛擬滾動條事件實現(xiàn)內容區(qū)滾動
監(jiān)聽虛擬滾動條的onmousedown事件,之后使用手柄偏移量handleOffset以及計算屬性handleTransform實現(xiàn)手柄的上下滑動
export default {
data () {
return {
...
handleOffset: 0,
}
},
computed: {
handleTransform () {
return `translateY(${this.handleOffset}px)`
}
},
methods: {
bindHandleEvent () {
const { $slider, $handle } = this.$element
const handleSpace = $slider.offsetHeight - this.handleHeight
$handle.onmousedown = (e) => {
const startY = e.clientY
const startTop = this.handleOffset
window.onmousemove = (e) => {
const deltaX = e.clientY - startY
this.handleOffset =
startTop + deltaX < 0
? 0
: Math.min(startTop + deltaX, handleSpace)
}
window.onmouseup = function () {
window.onmousemove = null
window.onmouseup = null
}
}
},
saveHtmlElementById () {
...
this.bindHandleEvent()
}
},
created () {
this.$nextTick(() => {
this.saveHtmlElementById()
})
}
}基本實現(xiàn)邏輯:在鼠標按下時記錄當前位置,鼠標移動則將移動值通過一定的轉換邏輯賦給手柄偏移量,同時限制手柄移動上下邊界
this.handleOffset =
startTop + deltaX < 0
? 0
: Math.min(startTop + deltaX, handleSpace)
最小為0,最大為handleSpace。
關聯(lián)手柄移動與內容區(qū)移動
到此處已經(jīng)實現(xiàn)了滾動條的移動和內容區(qū)的移動。但二者還是各自為戰(zhàn)的,需要關聯(lián)起來。具體關聯(lián)邏輯是關聯(lián)內容區(qū)最大滾動距離和虛擬滾動條最大移動距離。二者比例就是移動距離的數(shù)值關系。
增加關聯(lián)方法transferOffset
methods: {
transferOffset (to = 'handle') {
const { $container, $slider } = this.$element
const contentSpace = $container.scrollHeight - $container.offsetHeight
const handleSpace = $slider.offsetHeight - this.handleHeight
const assistRatio = handleSpace / contentSpace // 小于1
const _this = this
const computedOffset = {
handle () {
return -_this.contentOffset * assistRatio
},
content () {
return -_this.handleOffset / assistRatio
}
}
return computedOffset[to]()
}
}
contentSpace為內容最大滾動距離,handleSpace為手柄最大移動距離。assistRatio為二者比例。轉換對象computedOffset包含兩個方法,分別是通過內容移動距離轉為手柄移動距離和通過手柄移動距離轉為內容移動距離。使用轉換方法
methods: {
bindContainerEvent () {
...
const updateHandleOffset = () => {
// 使用關聯(lián)方法
this.handleOffset = this.transferOffset()
}
$container.addEventListener('wheel', bindContainerOffset)
// 給手柄事件在增加一個訂閱方法
$container.addEventListener('wheel', updateHandleOffset)
this.unbindContainerEvent = () => {
$container.removeEventListener('wheel', bindContainerOffset)
$container.removeEventListener('wheel', updateHandleOffset)
}
},
bindHandleEvent () {
const { $slider, $handle } = this.$element
const handleSpace = $slider.offsetHeight - this.handleHeight
$handle.onmousedown = (e) => {
const startY = e.clientY
const startTop = this.handleOffset
window.onmousemove = (e) => {
...
// 使用關聯(lián)方法
this.contentOffset = this.transferOffset('content')
}
window.onmouseup = function () {
window.onmousemove = null
window.onmouseup = null
}
}
}
},
beforeDestroy () {
this.unbindContainerEvent()
}
到此虛擬滾動基本實現(xiàn),看下效果

優(yōu)化
動態(tài)設置手柄高度
默認將手柄高度設置為20px,這實際是不符合實際滾動條高度變化規(guī)則的。實際內容區(qū)高度和內容區(qū)盒子高度相差越大則手柄高度越小反之越大。本文虛擬滾動為了方便操作可以人為限制手柄最小高度。
優(yōu)化手柄的高度邏輯,增加手柄高度屬性,以及計算屬性handleStyleHeight,限制手柄最小尺寸為20px,同時再增加手柄高度的初始化方法initHandleHeight
<template>
<div id="vs-container" ref="container">
<div id="vs-content" :style="{ transform: contentTransform }">
<p :key="num" v-for="num in list">{{ num }}</p>
</div>
<div id="vs-slider" ref="slider">
<div
id="vs-handle"
:style="{ transform: handleTransform, height: handleStyleHeight }"
ref="handle"
></div>
</div>
</div>
</template>
<script>
const HandleMixHeight = 20
export default {
data () {
return {
...
handleHeight: HandleMixHeight
}
},
computed: {
...
handleStyleHeight () {
return `${this.handleHeight}px`
}
},
methods: {
...
initHandleHeight () {
const { $container, $slider } = this.$element
// 根據(jù)比例變化
this.handleHeight =
($slider.offsetHeight * $container.offsetHeight) /
$container.scrollHeight
// 最小值為HandleMixHeight
if (this.handleHeight < HandleMixHeight) {
this.handleHeight = HandleMixHeight
}
}
},
created () {
this.$nextTick(() => {
this.saveHtmlElementById()
})
}
}
</script>禁止選中文本
在上文中的效果圖中也可以看出,當鼠標拖動滾動條時,內容區(qū)文本被選中了。這樣體驗很不好,對手柄和滑道添加禁止選中,使用css實現(xiàn)
<style lang="scss" scoped>
#vs-container {
...
#vs-slider {
...
-webkit-user-select: none; /* Safari/Chrome */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Standard */
#vs-handle {
...
-webkit-user-select: none; /* Safari/Chrome */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Standard */
}
}
}
</style>總結
本文是對虛擬滾動的一種實現(xiàn)。具體是通過對wheel事件的監(jiān)聽模擬內容的移動;通過對onmousedown、onmousemove、onmouseup的監(jiān)聽實現(xiàn)虛擬滾動條的移動。當然不管是內容的移動還是虛擬滾動條的移動都需要在一個閉區(qū)間內。
本文有2個沒有處理的點
- 不需要滾動條的情況
- 滾動條手柄的上下部分

感興趣可以進一步完善。本文的重點是垂直方向虛擬滾動的基本實現(xiàn),是為后面不定高虛擬列表服務。
以上就是vue實現(xiàn)虛擬滾動的示例詳解的詳細內容,更多關于vue虛擬滾動的資料請關注腳本之家其它相關文章!
相關文章
Vue-router 中hash模式和history模式的區(qū)別
這篇文章主要介紹了Vue-router 中hash模式和history模式的區(qū)別,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-07-07
詳解vue-Resource(與后端數(shù)據(jù)交互)
本篇文章主要介紹了vue-Resource(與后端數(shù)據(jù)交互),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01
基于Vue3和Element Plus實現(xiàn)自動導入功能
在 Vue 3 項目中,結合 Element Plus 實現(xiàn)自動導入可以顯著減少代碼量,提升開發(fā)效率,Element Plus 提供了官方的自動導入插件 unplugin-vue-components 和 unplugin-auto-import,以下是如何配置和使用的詳細步驟,需要的朋友可以參考下2025-03-03

