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-07Vue中$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-01vue實(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-03Vue使用vue-pdf實(shí)現(xiàn)PDF文件預(yù)覽
這篇文章主要為大家詳細(xì)介紹了Vue如何使用vue-pdf實(shí)現(xiàn)PDF文件預(yù)覽的功能,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,感興趣的可以了解一下2023-03-03