使用 Vue 实现一个虚拟列表的方法(3)

有了这套存储机制之后,我们就可以更高效地管理列表项的高度,和统计列表高度了。

看代码理解:

import { create as createSparseRangeList } from './SparseRangeList'
// 创建一个默认预估高度为 20 的列表项存储对象
const itemHeightStore = createSparseRangeList(wrappedItems.length, 20)
// 设置第二项为 40px
itemHeightStore.setValue(1, 40)
// 获取第二项的高度
itemHeightStore.getValueAt(1) // 40
// 获取列表项的 top 坐标
const top = (index: number): number => {
 if (index === 0) return 0
 // 0 ~ 上一项的高度累加
 const rangeValues = itemHeightStore.intersecting(0, index - 1)
 const sumHeight = rangeValues.reduce((sum: number, rangeValue: any) => {
  const span = rangeValue.end - rangeValue.start + 1
  return sum + rangeValue.value * span
 }, 0)
 return sumHeight
}
top(1) // 20
// 计算列表总高度:
const listHeight = itemHeightStore
 .values()
 .reduce((acc: number, rangeValue: any) => {
  const span = rangeValue.end - rangeValue.start + 1
  const height = rangeValue.value * span
  return acc + height
 }, 0)

计算可视条目

完成了列表项高度的管理,接下来需要解决的重点,就是计算出哪些条目是可视的。

最简单的实现方式,就是直接遍历我们的结点高度存储列表,逐个去跟视口的坐标区间比较,过滤出落在(或部分落在)视口内部的条目。 基于性能考虑,我们当然不能这么简单粗暴。我们可以做以下尝试来提高性能:

一、预估起点条目 + 二分法修正。

通过条目的预估高度或默认高度,算出可能出现在视口的第一条条目。 比如,我们视口上沿坐标(即滚动条滚过的距离)为 100px,我们条目预估高度为 20px,那么,我们可以猜测第一个出现在视口中的条目为 100 / 20 + 1,即第 6 条。 我们直接计算第 6 条的坐标,检查是否落在视口中,根据结果差距,再进行二分法猜测,直到找到真正的起点条目。

二、预估终点条目 + 二分法修正

在算出起点条目后,在使用视口高度除以预估条目高度,算出视口内部可能显示多少项,将起点序号加上这个数量,就是预估的终点条目序号。使用上述一样的修正逻辑,直到找到正确的视口终点条目。

描述可能比较难以理解,下面给出关键片段:

// 内部方法,计算局部渲染数据切片的起止点
private _calcSliceRange() {
 if (!this.dataView.length) {
  return { sliceFrom: 0, sliceTo: 0 }
 }
 // 数据总量
 const MAX = this.dataView.length
 // 视口上边界
 const viewportTop = (this.$refs.viewport as any).scrollTop || 0
 // 视口下边界
 const viewportBottom = viewportTop + this.viewportHeight
 // 预估条目高度
 const estimatedItemHeight = this.defaultItemHeight
 // 从估算值开始计算起始序号
 let sliceFrom = Math.floor(viewportTop / estimatedItemHeight!)
 if (sliceFrom > MAX - 1) sliceFrom = MAX - 1
 while (sliceFrom >= 0 && sliceFrom <= MAX - 1) {
  const itemTop = this._top(sliceFrom)
  // 条目顶部相对于 viewport 顶部的偏移
  const itemOffset = itemTop - viewportTop
  // 1. 该条目距离视口顶部有距离,说明上方还有条目元素需要显示,继续测试上一条
  if (itemOffset > 0) {
   // 二分法快速估算下一个尝试位置
   const diff = itemOffset / estimatedItemHeight!
   sliceFrom -= Math.ceil(diff / 2)
   continue
  }
  // 2. 恰好显示该条目的顶部,则该条目为本次视口的首条元素
  if (itemOffset === 0) break
  // 以下都是 itemOffset < 0
  const itemHeight = this._itemHeight(sliceFrom)
  // 3. 该条目在顶部露出了一部分,则该条目为本次视口的首条元素
  if (itemOffset < itemHeight) break
  // 4. 该条目已被滚出去视口,继续测试下一条
  // 二分法快速估算下一个尝试位置
  const diff = -itemOffset / estimatedItemHeight!
  sliceFrom += Math.ceil(diff / 2)
 }
 // 从估算值开始计算结束序号
 let sliceTo = sliceFrom + 1 + Math.floor(this.viewportHeight / estimatedItemHeight!)
 if (sliceTo > MAX) sliceTo = MAX
 while (sliceTo > sliceFrom && sliceTo <= MAX) {
  const itemTop = this._top(sliceTo)
  const itemHeight = this._itemHeight(sliceTo)
  const itemBottom = itemTop + itemHeight
  // 条目底部相对于 viewport 底部的偏移
  const itemOffset = itemBottom - viewportBottom
  // 1. 该条目的底部距离视口底部有距离,说明下方还有条目元素需要显示,继续测试下一条
  if (itemOffset < 0) {
   // 二分法快速估算下一个尝试位置
   const diff = -itemOffset / estimatedItemHeight!
   sliceTo += Math.ceil(diff / 2)
   continue
  }
  // 2. 恰好显示该条目的底部,则该条目为视口中最后一项
  if (itemOffset === 0) break
  // 3. 该条目在底部被裁剪了一部分,则该条目为本次视口的末项
  if (itemOffset < itemHeight) break
  // 该条目还未出场,继续测试上一条
  // 二分法快速估算下一个尝试位置
  const diff = itemOffset / estimatedItemHeight!
  sliceTo -= Math.ceil(diff / 2)
 }
 // slice 的时候,不含 end,所以 + 1
 sliceTo += 1
 return { sliceFrom, sliceTo }
}
      

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/459.html