vue实现虚拟滚动的示例详解

 更新时间:2023年10月25日 08:35:56   作者:通往自由之路  
虚拟滚动或者移动是指禁止原生滚动,之后通过监听浏览器的相关事件实现模拟滚动,下面小编就来和大家详细介绍一下vue实现虚拟滚动的示例代码,需要的可以参考下

虚拟滚动或者移动是指禁止原生滚动,之后通过监听浏览器的相关事件实现模拟滚动。所以虚拟滚动包含两部分内容

1.禁止原生滚动:将cssoverfow属性设置为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.模拟滚动:通过监听鼠标的wheel事件,调整内容位置,从而形成滚动效果;通过监听onmousedownonmousemoveonmouseup实现虚拟滚动条的移动

解决什么问题

  • 服务虚拟列表,尤其不定高度内容的虚拟列表实现;不定高内容虚拟列表在滑动过程中由于滚动速度大于渲染速度导致过快滑动时出现白屏现象。如果有虚拟滚动,则可以先进行数据渲染待渲染完毕再进行滚动,这样就彻底解决了白屏问题。
  • 在我工作中遇到使用虚拟列表实现不定高数据渲染问题,正好也出现了白屏问题

Dom结构

本文使用vue2实现虚拟滚动,DOM结构以及一些初始化数据如下

内容和盒子

<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,并且禁止盒子的原生滚动,设置盒子overflowhiddencontentTransform用来动态变化滚动位置。给盒子增加ref,标记container为后面开发使用。

虚拟滚动条

在上述代码中添加虚拟滚动条,虚拟滚动条包括滑道,其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用来动态变化虚拟滚动条的滚动位置,设置滚动条高度20px。到此处整个虚拟滚动示例长这样

虚拟滚动实现

实现虚拟滚动,开头说了模拟滚动原理:通过监听鼠标的wheel事件,调整内容位置,从而形成滚动效果;通过监听onmousedownonmousemoveonmouseup实现虚拟滚动条的移动。

本文使用translateY值的变化实现内容区或虚拟滚动条的滚动。本文只实现垂直方向上的滚动,水平方向上的滚动原理基本一致。

监听鼠标滚轮或触屏版实现内容区滚动

使用上文中ref获取相应的dom元素,然后给内容区盒子container绑定wheel事件。

监听wheel事件获取事件对象的wheelDeltaY,其含义为

返回一个整型数,表示垂直滚动量。

在谷歌浏览器下,如果是触屏版滑动返回0、1、2、3……或者0、-1、-2、-3……,如果是鼠标滚轮滚动返回150或-150。具体实现内容区滚动

<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值为负值,表示内容区向上滚动,反之内容区向下滚动。之后需要限制滚动区间

if (this.contentOffset < 0) {
   this.contentOffset = Math.max(this.contentOffset, -contentSpace)
} else {
   this.contentOffset = 0
}

内容区向上移动的最大距离为contentSpace,向下滚动的最大距离为0。

监听虚拟滚动条事件实现内容区滚动

监听虚拟滚动条的onmousedown事件,之后使用手柄偏移量handleOffset以及计算属性handleTransform实现手柄的上下滑动

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()
    })
  }
}

基本实现逻辑:在鼠标按下时记录当前位置,鼠标移动则将移动值通过一定的转换逻辑赋给手柄偏移量,同时限制手柄移动上下边界

this.handleOffset =
            startTop + deltaX < 0
              ? 0
              : Math.min(startTop + deltaX, handleSpace)

最小为0,最大为handleSpace

关联手柄移动与内容区移动

到此处已经实现了滚动条的移动和内容区的移动。但二者还是各自为战的,需要关联起来。具体关联逻辑是关联内容区最大滚动距离和虚拟滚动条最大移动距离。二者比例就是移动距离的数值关系。

增加关联方法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 = () => {
        // 使用关联方法
        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) => {
          ...
          // 使用关联方法
          this.contentOffset = this.transferOffset('content')
        }

        window.onmouseup = function () {
          window.onmousemove = null
          window.onmouseup = null
        }
      }
    }
  },
  beforeDestroy () {
    this.unbindContainerEvent()
  }

到此虚拟滚动基本实现,看下效果

优化

动态设置手柄高度

默认将手柄高度设置为20px,这实际是不符合实际滚动条高度变化规则的。实际内容区高度和内容区盒子高度相差越大则手柄高度越小反之越大。本文虚拟滚动为了方便操作可以人为限制手柄最小高度。

优化手柄的高度逻辑,增加手柄高度属性,以及计算属性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
      // 根据比例变化
      this.handleHeight =
        ($slider.offsetHeight * $container.offsetHeight) /
        $container.scrollHeight
      // 最小值为HandleMixHeight
      if (this.handleHeight < HandleMixHeight) {
        this.handleHeight = HandleMixHeight
      }
    }
  },
  created () {
    this.$nextTick(() => {
      this.saveHtmlElementById()
    })
  }
}
</script>

禁止选中文本

在上文中的效果图中也可以看出,当鼠标拖动滚动条时,内容区文本被选中了。这样体验很不好,对手柄和滑道添加禁止选中,使用css实现

<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>

总结

本文是对虚拟滚动的一种实现。具体是通过对wheel事件的监听模拟内容的移动;通过对onmousedownonmousemoveonmouseup的监听实现虚拟滚动条的移动。当然不管是内容的移动还是虚拟滚动条的移动都需要在一个闭区间内。

本文有2个没有处理的点

  • 不需要滚动条的情况
  • 滚动条手柄的上下部分

感兴趣可以进一步完善。本文的重点是垂直方向虚拟滚动的基本实现,是为后面不定高虚拟列表服务。

以上就是vue实现虚拟滚动的示例详解的详细内容,更多关于vue虚拟滚动的资料请关注脚本之家其它相关文章!

相关文章

  • VUE中v-model和v-for指令详解

    VUE中v-model和v-for指令详解

    本篇文章主要介绍了VUE中v-model和v-for指令详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • Vuex(多组件数据共享的Vue插件)搭建与使用

    Vuex(多组件数据共享的Vue插件)搭建与使用

    Vuex是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享,数据缓存等等,下面这篇文章主要给大家介绍了关于Vuex(多组件数据共享的Vue插件)搭建与使用的相关资料,需要的朋友可以参考下
    2022-10-10
  • Vue+Koa2+mongoose写一个像素绘板的实现方法

    Vue+Koa2+mongoose写一个像素绘板的实现方法

    这篇文章主要介绍了Vue+Koa2+mongoose写一个像素绘板的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • 基于vue3开发mobile-table适用于移动端表格

    基于vue3开发mobile-table适用于移动端表格

    这篇文章主要给大家介绍了关于如何基于vue3开发mobile-table适用于移动端表格的相关资料,需要的朋友可以参考下
    2024-03-03
  • vue 中几种传值方法(3种)

    vue 中几种传值方法(3种)

    这篇文章主要介绍了vue 中几种传值方法(3种),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-11-11
  • 详解vue2路由vue-router配置(懒加载)

    详解vue2路由vue-router配置(懒加载)

    本篇文章主要介绍了详解vue2路由vue-router配置(懒加载),实例分析了vue-router懒加载的技巧,非常具有实用价值,需要的朋友可以参考下
    2017-04-04
  • Vue keep-alive的实现原理分析

    Vue keep-alive的实现原理分析

    这篇文章主要介绍了Vue keep-alive的实现原理分析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • vue实现登录数据的持久化的使用示例

    vue实现登录数据的持久化的使用示例

    在Vue.js中,实现登录数据的持久化需要使用浏览器提供的本地存储功能,Vue.js支持使用localStorage和sessionStorage来实现本地存储,本文就来介绍一下如何实现,感兴趣的可以了解一下
    2023-10-10
  • vue使用canvas画布实现平面图点位标注功能(最新推荐)

    vue使用canvas画布实现平面图点位标注功能(最新推荐)

    这篇文章主要介绍了vue使用canvas画布实现平面图点位标注功能,经过本文一番研究发现canvas标签可以完成很多功能,电子签名,点位标注,问题标注,画图功能,感兴趣的朋友跟随小编一起看看吧
    2023-07-07
  • 如何为vuex实现带参数的 getter和state.commit

    如何为vuex实现带参数的 getter和state.commit

    这篇文章主要介绍了如何为vuex实现带参数的getter和state.commit,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2019-01-01

最新评论