vue实现不定高虚拟列表的示例详解

 更新时间:2023年10月31日 16:04:21   作者:通往自由之路  
这篇文章主要为大家详细介绍了在vue环境单页面项目下,如何实现不定高虚拟列表,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下

虚拟列表主要解决大数据量数据一次渲染性能差的问题。

之前写过一篇关于虚拟列表实现的文章:造轮子之不同场景下虚拟列表实现,主要讲了定高(高度统一和高度不统一两种情况)虚拟列表的实现,本文着重研究不定高虚拟列表的实现。在vue环境单页面项目下研究实现。

前文讲过虚拟列表的要做的事是确保性能的前提下,利用一定的技术模拟全数据一次性渲染后效果。

定高虚拟列表原理

绿色部分为containter,也就是父容器,它会有固定的高度。黄色部分为content,它是父容器的子元素。

当content的高度超过父容器的高度,就可以滚动内容区了,这就是一般滚动原理。

虚拟列表需要使用这个滚动原理。虚拟列表使用占位div,设置占位div的高度为所有列表数据的高度进而撑开containter,形成滚动条。

然后虚拟列表具体渲染过程中,只是渲染可视区也就是父容器区域

至于可视区域的内容滚动通过监听滚动条scroll事件,获取到滚动距离scrllTop,转换为可视区域的偏移位置,同时获取渲染数据的起始和结束索引,渲染指定段数据形成假象的滚动。

不定高内容数渲染

上一篇文章造轮子之不同场景下虚拟列表实现已经给出了定高虚拟列表的实现。不定高相对定高的难点在于数据没有渲染之前根本不知道数据的实际高度,解决方案理论上有

  • 在屏幕外渲染,但消耗性能
  • 以预估高度先行渲染,然后获取真实高度并缓存

采用第一种方案显然是不完美的,所以采用第二个方案,这也是之前有人实现过的。

不定高假数据

为了更接近业务,这里使用vue-codemirror方式渲染数据,为vue-codemirror造假数据

function generateRandomNumber () {
  const min = 100
  const max = 1000
  // 生成随机整数
  const randomNumber = Math.floor(Math.random() * (max - min + 1)) + min
  return randomNumber
}
function getRandomLetter () {
  const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  const randomIndex = Math.floor(Math.random() * letters.length)
  const randomLetter = letters.charAt(randomIndex)
  return randomLetter
}
function generateString (length) {
  const minLength = 100
  const maxLength = 1000

  // 确保长度在最小和最大范围内
  if (length < minLength) {
    length = minLength
  } else if (length > maxLength) {
    length = maxLength
  }

  // 生成字符串
  const string = getRandomLetter().repeat(length)

  return string
}
const d = []
for (let i = 0; i < 500; i++) {
  const length = generateRandomNumber()
  d.push({
    data: generateString(length),
    index: i
  })
}

这里造了500条,具体是随机生成的字符串,字符串长度100-1000,字符从A-Z中选取。

温故定高虚拟列表

因为不定高虚拟列表有和定高虚拟列表相似之处,再来回顾一下之前定高(统一高度和不统一高度)的解决方案。这里只展示一下统一高度的,不统一高度的可以查看造轮子之不同场景下虚拟列表实现。统一高度组件代码

<template>
  <div ref="list" class="render-list-container" @scroll="scrollEvent($event)">
    <!-- 占位div -->
    <div class="render-list-phantom" :style="{ height: listHeight + 'px' }"></div>
    <div class="render-list" :style="{ transform: getTransform }">
      <template
        v-for="item in visibleData"
      >
        <slot :value="item.value"  :height="itemSize + 'px'"  :index="item.id"></slot>
      </template>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VirtualList',
  props: {
    // 所有列表数据
    listData: {
      type: Array,
      default: () => []
    },
    // 每项高度
    itemSize: {
      type: Number,
      default: 100
    }
  },
  computed: {
    // 列表总高度
    listHeight () {
      return this.listData.length * this.itemSize
    },
    // 可显示的列表项数
    visibleCount () {
      return Math.ceil(this.screenHeight / this.itemSize)
    },
    // 偏移量对应的style
    getTransform () {
      return `translate3d(0,${this.startOffset}px,0)`
    },
    // 获取真实显示列表数据
    visibleData () {
      return this.listData.slice(this.start, Math.min(this.end, this.listData.length))
    }
  },
  mounted () {
    this.screenHeight = this.$el.clientHeight
    this.end = this.start + this.visibleCount
  },
  data () {
    return {
      // 可视区域高度
      screenHeight: 0,
      // 偏移量
      startOffset: 0,
      // 起始索引
      start: 0,
      // 结束索引
      end: null
    }
  },
  methods: {
    scrollEvent () {
      // 当前滚动位置
      const scrollTop = this.$refs.list.scrollTop
      // 此时的开始索引
      this.start = Math.floor(scrollTop / this.itemSize)
      // 此时的结束索引
      this.end = this.start + this.visibleCount
      // 此时的偏移量
      this.startOffset = scrollTop - (scrollTop % this.itemSize)
    }
  }
}
</script>

<style scoped>
.render-list-container {
  overflow: auto;
  position: relative;
  -webkit-overflow-scrolling: touch;
  height: 200px;
}

.render-list-phantom {
  position: absolute;
  left: 0;
  right: 0;
  z-index: -1;
}

.render-list {
  text-align: center;
}

</style>

研究不定高虚拟列表组件

按照统一高度方式渲染

正如上面所说为了解决不定高内容高度不定的问题,采用

以预估高度先行渲染,然后获取真实高度并缓存方案

所以给每条假数据一条预估高度,然后使用定高虚拟列表渲染数据,渲染数据代码

<template>
  <div class="render-show">
    <div>
      <NoHasVirtualList :listData="data">
        <template slot-scope="{ item, height }">
          <codemirror
            class="unit"
            :style="{height: height}" 
            v-model="item.data"
            :options="cmOptions"
          ></codemirror>
        </template>
      </NoHasVirtualList>
    </div>
  </div>
</template>

设置codemirror组件高度固定。查看一下效果

定高下查看.gif

问题很明显,由于codemirror组件设置固定高度,导致渲染内容挤到一起了,分不清哪个是哪个高度方向出现重合。所以预估高度不是这样用的,预估高度的意义:它是一种高度占位,是一种占位是务必要修正的。

修正高度

为了修正这个高度,需要等待数据渲染后拿到真实高度,这个需求可以在vue生命周期函数updated实现,也可以通过IntersectionObserver实现。本文采用updated实现。

修正高度不仅修正每一条数据的高度,因为用来撑起可视区域的占位div高度也是根据预估高度计算的,所以占位div高度也需要更新,然后还需要更新偏移量。

具体在updated里获取真实元素大小,修改对应的尺寸缓存;更新占位div高度(使用计算属性实现);更新真实偏移量。

  updated () {
    this.$nextTick(() => {
      // 获取真实元素大小,修改对应的尺寸缓存
      this.updateItemsSize()

      // 更新真实偏移量
      this.setStartOffset()
    })
  },

获取数据实际高度,修改对应尺寸缓存

创建计算属性_listData拷贝列表数据。目的尽量不修改传进来的listData列表数据,同时给渲染列表数据添加索引,实际是给渲染用的visibleCount添加唯一索引

  computed: {
    _listData () {
      return this.listData.reduce((init, cur, index) => {
        init.push({
          // _转换后的索引
          _key: index,
          value: cur
        })
        return init
      }, [])
    },
   ...
  }

缓存每条数据的高度、以及数据坐标:用topbottom标记

   // 初始化缓存
    initPositions () {
      this.positions = this._listData.map((d, index) => ({
        index,
        height: this.itemSize,
        top: index * this.itemSize,
        bottom: (index + 1) * this.itemSize
      }))
    },

上面计算属性_listData以及缓存每条数据均是服务于这一步:获取渲染数据实际高度,修改对应数据缓存尺寸

    // 获取实际高度,修正内容高度
    updateItemsSize () {
      const nodes = this.$refs.items
      nodes.forEach((node) => {
        // 获取元素自身的属性
        const rect = node.getBoundingClientRect()
        const height = rect.height
        const index = +node.id // id就是_listData上的唯一索引
        const oldHeight = this.positions[index].height
        const dValue = oldHeight - height
        // 存在差值
        if (dValue) {
          this.positions[index].bottom = this.positions[index].bottom - dValue
          this.positions[index].height = height
          this.positions[index].over = true // TODO

          for (let k = index + 1; k < this.positions.length; k++) {
            this.positions[k].top = this.positions[k - 1].bottom
            this.positions[k].bottom = this.positions[k].bottom - dValue
          }
        }
      })
    },

更新列表总高度

获取数据实际高度,修改对应尺寸缓存目的之一是为了更新列表总高度

  computed: {
    ...
    // 列表总高度
    listHeight () {
      return this.positions[this.positions.length - 1].bottom
    },
    ...
  },

上述代码中this.listHeight是一个计算属性,是占位div的高度。

<template>
      <div
        ref="list"
        class="infinite-list-container"
        @scroll="scrollEvent($event)"
      >
        <!-- 占位div -->
        <div ref="phantom" class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
       ...
      </div>
  </template>

更新真实偏移量

获取数据实际高度,修改对应尺寸缓存目的之二是为了更新真实偏移量。

借助this.positions数组数据,通过设置this.startOffset,在传导到计算属性this.contentTransform更新偏移量

<template>
    <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
      <!-- 占位div -->
      <div
        class="infinite-list-phantom"
        :style="{ height: listHeight + 'px' }"
      ></div>

      <div
        ref="content"
        :style="{ transform: contentTransform }"
        class="infinite-list"
      >
      ....
      </div>
    </div>
  </template>
  ...
 computed: {
    ...
    // 偏移量对应的style
    contentTransform () {
      return `translateY(${this.startOffset}px)`
    },
     ...
   },
   ...
    // 更新偏移量
    setStartOffset () {
      if (this.start >= 1) {
        const size =
            this.positions[this.start].top -
            (this.positions[this.start - this.aboveCount]
              ? this.positions[this.start - this.aboveCount].top
              : 0)
        this.startOffset = this.positions[this.start - 1].bottom - size
      } else {
        this.startOffset = 0
      }
    }

滚动事件

滚动事件用以触发更新

   // 滚动事件
    scrollEvent () {
      // 当前滚动位置
      const scrollTop = this.$refs.list.scrollTop
      // 更新滚动状态
      // 排除不需要计算的情况
      if (
        scrollTop > this.anchorPoint.bottom ||
          scrollTop < this.anchorPoint.top
      ) {
        // 此时的开始索引
        this.start = this.getStartIndex(scrollTop)
        // 此时的结束索引
        this.end = this.start + this.visibleCount
        // 更新偏移量
        this.setStartOffset()
      }
    }

其中this.anchorPoint是计算属性

  computed: {
    ...
    anchorPoint () {
      return this.positions.length ? this.positions[this.start] : null
    }
    ...
  },

上述代码中之所以排除不需要计算的情况,需要解释一下。

真实的滚动就是滚动条滚动了多少,可视区就向上移动多少。但虚拟滚动不是。当起始索引发生变化时,渲染数据发生变化了,但渲染数据的高度不是连续的,所以需要动态的设置偏移量。当滚动时起始索引不发生变化时,因为数据变化是连续的,此时可以什么也不做,滚动显示的内容由浏览器控制。排除的部分就是索引没发生变化的情况。

根据滚动高度获取起始索引方法this.getStartIndex

  methods: {
    ...
    // 获取列表起始索引
    getStartIndex (scrollTop = 0) {
      // 二分法查找
      return this.binarySearch(this.positions, scrollTop)
    },
    // 二分法查找 用于查找开始索引
    binarySearch (list, value) {
      let start = 0
      let end = list.length - 1
      let tempIndex = null

      while (start <= end) {
        const midIndex = parseInt((start + end) / 2)
        const midValue = list[midIndex].bottom
        if (midValue === value) {
          return midIndex + 1
        } else if (midValue < value) {
          start = midIndex + 1
        } else if (midValue > value) {
          if (tempIndex === null || tempIndex > midIndex) {
            tempIndex = midIndex
          }
          end = end - 1
        }
      }
      return tempIndex
    },
    ...  
  }

效果查看以及优化

虚拟.gif

给滚动增加缓冲,缓冲就是多渲染几条,上方和下方渲染额外的数据,比如前后多渲染2条。增加计算属性aboveCountbelowCount,同时修改visibleData

  computed: {
    ...
    aboveCount () {
      return Math.min(this.start, 2)
    },
    belowCount () {
      return Math.min(this.listData.length - this.end, 2)
    },
    visibleData () {
      const start = this.start - this.aboveCount
      const end = this.end + this.belowCount
      return this._listData.slice(start, end)
    }
  },

存在问题

即便是给滚动增加缓冲,过快滑动时依然会出现白屏现象,究其本质是滚动过快而真实dom更新赶不上它

有多余.gif

总结

本文主要研究了不定高虚拟列表的一种实现。基本原理依然是原生滚动触发,渲染首先是预估高度,之后数据渲染后更新预估高度、更新占位div高度、更新偏移量。

另外就是对于滚动事件做限制,如果滚动高度恰好位于当前元素范围内不做处理。

另外对于数据更新除了可以使用vue的生命周期函数updated还可以使用IntersectionObserver实现。

后期计划:为了解决过快滑动导致的白屏现象,会将不定高虚拟列表与虚拟滚动结合。虚拟滚动前几天写过一篇实现方案:虚拟滚动实现

可优化的方案:

  • 采用多线程更新方法this.updateItemsSize里内容
  • 使用css隐藏原生滚动条,模拟出一个新滚动条,人为控制新滚动条的滚动速度
 .infinite-list-container::-webkit-scrollbar {
  width:0;
 }

本项目代码地址:github.com/zhensg123/rareRecord/tree/main/virtual-list

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

相关文章

  • 手写Vue源码之数据劫持示例详解

    手写Vue源码之数据劫持示例详解

    这篇文章主要给大家介绍了手写Vue源码之数据劫持的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • vue项目前端知识点整理【收藏】

    vue项目前端知识点整理【收藏】

    本文是小编给大家收藏整理的关于vue项目前端知识点,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-05-05
  • vue中slot插槽的参数汇总及使用方案

    vue中slot插槽的参数汇总及使用方案

    Vue.js中的插槽(slot)是一种机制,允许你在组件的模板中预留一些位置,以便父组件可以将任意内容插入到这些位置,这使得组件更加灵活和可复用,本文主要介绍了vue中slot插槽的参数汇总及使用方案,需要的朋友可以参考下
    2024-03-03
  • vue中的适配px2rem示例代码

    vue中的适配px2rem示例代码

    这篇文章主要给大家介绍了关于vue中适配px2rem的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-11-11
  • Vue路由传参及props解耦深入分析

    Vue路由传参及props解耦深入分析

    vue路由传参的使用场景一般都是应用在父路由跳转到子路由时,携带参数跳转,下面这篇文章主要给大家介绍了关于vue路由传参方式的方式总结及获取参数的相关资料,文中通过实例代码介绍的非常详细,需要的朋友可以参考下
    2022-07-07
  • vue使用refs获取嵌套组件中的值过程

    vue使用refs获取嵌套组件中的值过程

    这篇文章主要介绍了vue使用refs获取嵌套组件中的值过程,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • vue结合AntV G2的使用踩坑记录

    vue结合AntV G2的使用踩坑记录

    这篇文章主要介绍了vue结合AntV G2的使用踩坑记录,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-04-04
  • axios二次封装的详细过程与跨域问题

    axios二次封装的详细过程与跨域问题

    通常我们的项目会越做越大,页面也会越来越多,随之而来的是接口数量的增加,api统一管理,不管接口有多少,所有的接口都可以非常清晰,容易维护,下面这篇文章主要给大家介绍了关于axios二次封装的详细过程与跨域问题的相关资料,需要的朋友可以参考下
    2022-09-09
  • vue动画效果实现方法示例

    vue动画效果实现方法示例

    这篇文章主要介绍了vue动画效果实现方法,结合完整实例形式分析了vue.js+animate.css实现的动画切换效果相关操作技巧,需要的朋友可以参考下
    2019-03-03
  • vue-cli3项目展示本地Markdown文件的方法

    vue-cli3项目展示本地Markdown文件的方法

    这篇文章主要介绍了vue-cli3项目展示本地Markdown文件的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-06-06

最新评论