vue實(shí)現(xiàn)虛擬滾動(dòng)的示例詳解
虛擬滾動(dòng)或者移動(dòng)是指禁止原生滾動(dòng),之后通過(guò)監(jiān)聽瀏覽器的相關(guān)事件實(shí)現(xiàn)模擬滾動(dòng)。所以虛擬滾動(dòng)包含兩部分內(nèi)容
1.禁止原生滾動(dòng):將css的overfow屬性設(shè)置為hidden。這樣即便是內(nèi)容高度或者寬度超過(guò)了盒子的寬度或者高度也無(wú)法進(jìn)行滾動(dòng)了
<div id="vs-container">
<div id="vs-content">
<p>內(nèi)容</p>
<p>內(nèi)容</p>
<p>內(nèi)容</p>
<p>內(nèi)容</p>
<p>內(nèi)容</p>
<p>內(nèi)容</p>
<p>內(nèi)容</p>
<p>內(nèi)容</p>
</div>
</div>
<style>
#vs-container {
overflow:hidden;
height:100px;
}
#vs-content {
height:200px;
}
</style>
2.模擬滾動(dòng):通過(guò)監(jiān)聽鼠標(biāo)的wheel事件,調(diào)整內(nèi)容位置,從而形成滾動(dòng)效果;通過(guò)監(jiān)聽onmousedown、onmousemove、onmouseup實(shí)現(xiàn)虛擬滾動(dòng)條的移動(dòng)
解決什么問(wèn)題
- 服務(wù)虛擬列表,尤其不定高度內(nèi)容的虛擬列表實(shí)現(xiàn);不定高內(nèi)容虛擬列表在滑動(dòng)過(guò)程中由于滾動(dòng)速度大于渲染速度導(dǎo)致過(guò)快滑動(dòng)時(shí)出現(xiàn)白屏現(xiàn)象。如果有虛擬滾動(dòng),則可以先進(jìn)行數(shù)據(jù)渲染待渲染完畢再進(jìn)行滾動(dòng),這樣就徹底解決了白屏問(wèn)題。
- 在我工作中遇到使用虛擬列表實(shí)現(xiàn)不定高數(shù)據(jù)渲染問(wèn)題,正好也出現(xiàn)了白屏問(wèn)題
Dom結(jié)構(gòu)
本文使用vue2實(shí)現(xiàn)虛擬滾動(dòng),DOM結(jié)構(gòu)以及一些初始化數(shù)據(jù)如下
內(nèi)容和盒子
<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>上述代碼內(nèi)容id為vs-content,盒子id為vs-container,盒子高度200px,并且禁止盒子的原生滾動(dòng),設(shè)置盒子overflow為hidden。contentTransform用來(lái)動(dòng)態(tài)變化滾動(dòng)位置。給盒子增加ref,標(biāo)記container為后面開發(fā)使用。
虛擬滾動(dòng)條
在上述代碼中添加虛擬滾動(dòng)條,虛擬滾動(dòng)條包括滑道,其ref設(shè)置為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用來(lái)動(dòng)態(tài)變化虛擬滾動(dòng)條的滾動(dòng)位置,設(shè)置滾動(dòng)條高度20px。到此處整個(gè)虛擬滾動(dòng)示例長(zhǎng)這樣

虛擬滾動(dòng)實(shí)現(xiàn)
實(shí)現(xiàn)虛擬滾動(dòng),開頭說(shuō)了模擬滾動(dòng)原理:通過(guò)監(jiān)聽鼠標(biāo)的wheel事件,調(diào)整內(nèi)容位置,從而形成滾動(dòng)效果;通過(guò)監(jiān)聽onmousedown、onmousemove、onmouseup實(shí)現(xiàn)虛擬滾動(dòng)條的移動(dòng)。
本文使用translateY值的變化實(shí)現(xiàn)內(nèi)容區(qū)或虛擬滾動(dòng)條的滾動(dòng)。本文只實(shí)現(xiàn)垂直方向上的滾動(dòng),水平方向上的滾動(dòng)原理基本一致。
監(jiān)聽鼠標(biāo)滾輪或觸屏版實(shí)現(xiàn)內(nèi)容區(qū)滾動(dòng)
使用上文中ref獲取相應(yīng)的dom元素,然后給內(nèi)容區(qū)盒子container綁定wheel事件。
監(jiān)聽wheel事件獲取事件對(duì)象的wheelDeltaY,其含義為
返回一個(gè)整型數(shù),表示垂直滾動(dòng)量。
在谷歌瀏覽器下,如果是觸屏版滑動(dòng)返回0、1、2、3……或者0、-1、-2、-3……,如果是鼠標(biāo)滾輪滾動(dòng)返回150或-150。具體實(shí)現(xiàn)內(nèi)容區(qū)滾動(dòng)
<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值為負(fù)值,表示內(nèi)容區(qū)向上滾動(dòng),反之內(nèi)容區(qū)向下滾動(dòng)。之后需要限制滾動(dòng)區(qū)間
if (this.contentOffset < 0) {
this.contentOffset = Math.max(this.contentOffset, -contentSpace)
} else {
this.contentOffset = 0
}
內(nèi)容區(qū)向上移動(dòng)的最大距離為contentSpace,向下滾動(dòng)的最大距離為0。
監(jiān)聽虛擬滾動(dòng)條事件實(shí)現(xiàn)內(nèi)容區(qū)滾動(dòng)
監(jiān)聽虛擬滾動(dòng)條的onmousedown事件,之后使用手柄偏移量handleOffset以及計(jì)算屬性handleTransform實(shí)現(xiàn)手柄的上下滑動(dòng)
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()
})
}
}基本實(shí)現(xiàn)邏輯:在鼠標(biāo)按下時(shí)記錄當(dāng)前位置,鼠標(biāo)移動(dòng)則將移動(dòng)值通過(guò)一定的轉(zhuǎn)換邏輯賦給手柄偏移量,同時(shí)限制手柄移動(dòng)上下邊界
this.handleOffset =
startTop + deltaX < 0
? 0
: Math.min(startTop + deltaX, handleSpace)
最小為0,最大為handleSpace。
關(guān)聯(lián)手柄移動(dòng)與內(nèi)容區(qū)移動(dòng)
到此處已經(jīng)實(shí)現(xiàn)了滾動(dòng)條的移動(dòng)和內(nèi)容區(qū)的移動(dòng)。但二者還是各自為戰(zhàn)的,需要關(guān)聯(lián)起來(lái)。具體關(guān)聯(lián)邏輯是關(guān)聯(lián)內(nèi)容區(qū)最大滾動(dòng)距離和虛擬滾動(dòng)條最大移動(dòng)距離。二者比例就是移動(dòng)距離的數(shù)值關(guān)系。
增加關(guān)聯(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為內(nèi)容最大滾動(dòng)距離,handleSpace為手柄最大移動(dòng)距離。assistRatio為二者比例。轉(zhuǎn)換對(duì)象computedOffset包含兩個(gè)方法,分別是通過(guò)內(nèi)容移動(dòng)距離轉(zhuǎn)為手柄移動(dòng)距離和通過(guò)手柄移動(dòng)距離轉(zhuǎn)為內(nèi)容移動(dòng)距離。使用轉(zhuǎn)換方法
methods: {
bindContainerEvent () {
...
const updateHandleOffset = () => {
// 使用關(guān)聯(lián)方法
this.handleOffset = this.transferOffset()
}
$container.addEventListener('wheel', bindContainerOffset)
// 給手柄事件在增加一個(gè)訂閱方法
$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) => {
...
// 使用關(guān)聯(lián)方法
this.contentOffset = this.transferOffset('content')
}
window.onmouseup = function () {
window.onmousemove = null
window.onmouseup = null
}
}
}
},
beforeDestroy () {
this.unbindContainerEvent()
}
到此虛擬滾動(dòng)基本實(shí)現(xiàn),看下效果

優(yōu)化
動(dòng)態(tài)設(shè)置手柄高度
默認(rèn)將手柄高度設(shè)置為20px,這實(shí)際是不符合實(shí)際滾動(dòng)條高度變化規(guī)則的。實(shí)際內(nèi)容區(qū)高度和內(nèi)容區(qū)盒子高度相差越大則手柄高度越小反之越大。本文虛擬滾動(dòng)為了方便操作可以人為限制手柄最小高度。
優(yōu)化手柄的高度邏輯,增加手柄高度屬性,以及計(jì)算屬性handleStyleHeight,限制手柄最小尺寸為20px,同時(shí)再增加手柄高度的初始化方法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>禁止選中文本
在上文中的效果圖中也可以看出,當(dāng)鼠標(biāo)拖動(dòng)滾動(dòng)條時(shí),內(nèi)容區(qū)文本被選中了。這樣體驗(yàn)很不好,對(duì)手柄和滑道添加禁止選中,使用css實(shí)現(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>總結(jié)
本文是對(duì)虛擬滾動(dòng)的一種實(shí)現(xiàn)。具體是通過(guò)對(duì)wheel事件的監(jiān)聽模擬內(nèi)容的移動(dòng);通過(guò)對(duì)onmousedown、onmousemove、onmouseup的監(jiān)聽實(shí)現(xiàn)虛擬滾動(dòng)條的移動(dòng)。當(dāng)然不管是內(nèi)容的移動(dòng)還是虛擬滾動(dòng)條的移動(dòng)都需要在一個(gè)閉區(qū)間內(nèi)。
本文有2個(gè)沒(méi)有處理的點(diǎn)
- 不需要滾動(dòng)條的情況
- 滾動(dòng)條手柄的上下部分

感興趣可以進(jìn)一步完善。本文的重點(diǎn)是垂直方向虛擬滾動(dòng)的基本實(shí)現(xiàn),是為后面不定高虛擬列表服務(wù)。
以上就是vue實(shí)現(xiàn)虛擬滾動(dòng)的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于vue虛擬滾動(dòng)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue-router 中hash模式和history模式的區(qū)別
這篇文章主要介紹了Vue-router 中hash模式和history模式的區(qū)別,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-07-07
Vue中$nextTick實(shí)現(xiàn)源碼解析
這篇文章主要為大家介紹了Vue中$nextTick實(shí)現(xiàn)源碼解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
詳解vue-Resource(與后端數(shù)據(jù)交互)
本篇文章主要介紹了vue-Resource(與后端數(shù)據(jù)交互),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01
vue實(shí)現(xiàn)接口封裝的實(shí)現(xiàn)示例
本文主要介紹了vue實(shí)現(xiàn)接口封裝的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-11-11
基于Vue3和Element Plus實(shí)現(xiàn)自動(dòng)導(dǎo)入功能
在 Vue 3 項(xiàng)目中,結(jié)合 Element Plus 實(shí)現(xiàn)自動(dòng)導(dǎo)入可以顯著減少代碼量,提升開發(fā)效率,Element Plus 提供了官方的自動(dòng)導(dǎo)入插件 unplugin-vue-components 和 unplugin-auto-import,以下是如何配置和使用的詳細(xì)步驟,需要的朋友可以參考下2025-03-03
Vue使用vue-pdf實(shí)現(xiàn)PDF文件預(yù)覽
這篇文章主要為大家詳細(xì)介紹了Vue如何使用vue-pdf實(shí)現(xiàn)PDF文件預(yù)覽的功能,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,感興趣的可以了解一下2023-03-03

