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

以上就是计算可视区域的核心部分。完整的代码,会在后续给出。

DOM 更新
由于我们是使用 Vue 来实现虚拟列表的,所以 DOM 的更新方面,可以省去大量繁琐的细节管理。 我们只需要关心列表滚动到某处之后,如何计算出当前视口应该出现哪些条目即可。

尽管如此,考虑到滚动的流畅性,以及 IE11 等浏览器的 DOM 操作性能,我们不得不多做很多事情。

批量 DOM 操作

我们可以在 IE11 的开发者工具面板中看到,滚动过程,频繁地往虚拟列表首尾插入、移除结点,会带来非常严重的性能问题。 所以,我们必须控制 DOM 操作的频率。

批量可以部分解决这个问题。

具体的思路是,在滚动回调中,我们计算出可视区域的结点起止序号,不直接应用,而是加上一个额外渲染的数量。 比如我们计算出当前应该渲染 20 ~ 30 这些条目,我们可以在前后各加上 10 个额外渲染的条目,即 10 ~ 40,这样就一次性渲染了 30 个结点。在继续滚动时,我们检查新的起止范围,是否还在 10 ~ 40 范围内,如果是,我们就不做新的结点增删操作。

核心实现:

// 刷新局部渲染数据切片范围
private _updateSliceRange(forceUpdate?: boolean) {
 // 上下方额外多渲染的条目波动量
 const COUNT = this._preRenderingCount()

 // 预渲染触发阈值
 const THRESHOLD = this._preRenderingThreshold()  

 // 数据总量
 const MAX = this.dataView.length

 // 计算出准确的切片区间
 const range = this._calcSliceRange()  

 // 检查计算出来的切片范围,是否被当前已经渲染的切片返回包含了
 // 如果是,无需更新切片,(如果 forceUpdate,则无论如何都需要重新切片)
 let fromThreshold = range.sliceFrom - THRESHOLD
 if (fromThreshold < 0) fromThreshold = 0
 let toThreshold = range.sliceTo + THRESHOLD
 if (toThreshold > MAX) toThreshold = MAX

 // 无需强制刷新,且上下两端都没有触达阈值时,无需重新切片
 if (!forceUpdate && ((this.sliceFrom <= fromThreshold) && (this.sliceTo >= toThreshold))) {
  return
 }

 // 下面是更新切片的情况

 // 在切片区间头部、尾部,追加预渲染的条目
 let { sliceFrom, sliceTo } = range
 sliceFrom = sliceFrom > COUNT ? sliceFrom - COUNT : 0
 sliceTo = sliceTo + COUNT > MAX ? MAX : sliceTo + COUNT

 this.sliceFrom = sliceFrom
 this.sliceTo = sliceTo
 if (forceUpdate) this._doSlice()
}

使用了这种批量操作之后,可以看到,正常的鼠标滚动下,IE 也能比较顺畅地滚动了。

事件

由于虚拟列表的 DOM 需要不停地生成和销毁,因此,直接在列表项目上绑定事件是非常低效的。 所以,使用事件代理就成了很不错的方案,将事件注册在组件根结点上,再根据 event.target 来区分是由哪个列表项冒泡出来的事件,即可高效处理。