vue實現(xiàn)虛擬滾動的示例詳解
虛擬滾動或者移動是指禁止原生滾動,之后通過監(jiān)聽瀏覽器的相關(guān)事件實現(xiàn)模擬滾動。所以虛擬滾動包含兩部分內(nèi)容
1.禁止原生滾動:將css
的overfow
屬性設置為hidden
。這樣即便是內(nèi)容高度或者寬度超過了盒子的寬度或者高度也無法進行滾動了
<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.模擬滾動:通過監(jiān)聽鼠標的wheel
事件,調(diào)整內(nèi)容位置,從而形成滾動效果;通過監(jiān)聽onmousedown
、onmousemove
、onmouseup
實現(xiàn)虛擬滾動條的移動
解決什么問題
- 服務虛擬列表,尤其不定高度內(nèi)容的虛擬列表實現(xiàn);不定高內(nèi)容虛擬列表在滑動過程中由于滾動速度大于渲染速度導致過快滑動時出現(xiàn)白屏現(xiàn)象。如果有虛擬滾動,則可以先進行數(shù)據(jù)渲染待渲染完畢再進行滾動,這樣就徹底解決了白屏問題。
- 在我工作中遇到使用虛擬列表實現(xiàn)不定高數(shù)據(jù)渲染問題,正好也出現(xiàn)了白屏問題
Dom結(jié)構(gòu)
本文使用vue2實現(xiàn)虛擬滾動,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
,并且禁止盒子的原生滾動,設置盒子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
事件,調(diào)整內(nèi)容位置,從而形成滾動效果;通過監(jiān)聽onmousedown
、onmousemove
、onmouseup
實現(xiàn)虛擬滾動條的移動。
本文使用translateY
值的變化實現(xiàn)內(nèi)容區(qū)或虛擬滾動條的滾動。本文只實現(xiàn)垂直方向上的滾動,水平方向上的滾動原理基本一致。
監(jiān)聽鼠標滾輪或觸屏版實現(xiàn)內(nèi)容區(qū)滾動
使用上文中ref獲取相應的dom元素,然后給內(nèi)容區(qū)盒子container
綁定wheel
事件。
監(jiān)聽wheel
事件獲取事件對象的wheelDeltaY
,其含義為
返回一個整型數(shù),表示垂直滾動量。
在谷歌瀏覽器下,如果是觸屏版滑動返回0、1、2、3……或者0、-1、-2、-3……,如果是鼠標滾輪滾動返回150或-150。具體實現(xiàn)內(nèi)容區(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
值為負值,表示內(nèi)容區(qū)向上滾動,反之內(nèi)容區(qū)向下滾動。之后需要限制滾動區(qū)間
if (this.contentOffset < 0) { this.contentOffset = Math.max(this.contentOffset, -contentSpace) } else { this.contentOffset = 0 }
內(nèi)容區(qū)向上移動的最大距離為contentSpace
,向下滾動的最大距離為0。
監(jiān)聽虛擬滾動條事件實現(xiàn)內(nèi)容區(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)邏輯:在鼠標按下時記錄當前位置,鼠標移動則將移動值通過一定的轉(zhuǎn)換邏輯賦給手柄偏移量,同時限制手柄移動上下邊界
this.handleOffset = startTop + deltaX < 0 ? 0 : Math.min(startTop + deltaX, handleSpace)
最小為0,最大為handleSpace
。
關(guān)聯(lián)手柄移動與內(nèi)容區(qū)移動
到此處已經(jīng)實現(xiàn)了滾動條的移動和內(nèi)容區(qū)的移動。但二者還是各自為戰(zhàn)的,需要關(guān)聯(lián)起來。具體關(guān)聯(lián)邏輯是關(guān)聯(lián)內(nèi)容區(qū)最大滾動距離和虛擬滾動條最大移動距離。二者比例就是移動距離的數(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)容最大滾動距離,handleSpace
為手柄最大移動距離。assistRatio
為二者比例。轉(zhuǎn)換對象computedOffset
包含兩個方法,分別是通過內(nèi)容移動距離轉(zhuǎn)為手柄移動距離和通過手柄移動距離轉(zhuǎn)為內(nèi)容移動距離。使用轉(zhuǎn)換方法
methods: { bindContainerEvent () { ... const updateHandleOffset = () => { // 使用關(guān)聯(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) => { ... // 使用關(guān)聯(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ī)則的。實際內(nèi)容區(qū)高度和內(nèi)容區(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>
禁止選中文本
在上文中的效果圖中也可以看出,當鼠標拖動滾動條時,內(nèi)容區(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>
總結(jié)
本文是對虛擬滾動的一種實現(xiàn)。具體是通過對wheel事件的監(jiān)聽模擬內(nèi)容的移動;通過對onmousedown
、onmousemove
、onmouseup
的監(jiān)聽實現(xiàn)虛擬滾動條的移動。當然不管是內(nèi)容的移動還是虛擬滾動條的移動都需要在一個閉區(qū)間內(nèi)。
本文有2個沒有處理的點
- 不需要滾動條的情況
- 滾動條手柄的上下部分
感興趣可以進一步完善。本文的重點是垂直方向虛擬滾動的基本實現(xiàn),是為后面不定高虛擬列表服務。
以上就是vue實現(xiàn)虛擬滾動的示例詳解的詳細內(nèi)容,更多關(guān)于vue虛擬滾動的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
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 項目中,結(jié)合 Element Plus 實現(xiàn)自動導入可以顯著減少代碼量,提升開發(fā)效率,Element Plus 提供了官方的自動導入插件 unplugin-vue-components 和 unplugin-auto-import,以下是如何配置和使用的詳細步驟,需要的朋友可以參考下2025-03-03