读vue-virtual-scroller 源码

5,672 阅读2分钟

最近回顾下长列表的一些优化方案,提起长列表,vue栈最多的就会涉及到 vue-virtual-scroller 此轮子。

为了理清更多“为什么”,于是对插件源码进行简单分析。

看到源码目录,主要有 RecycleScroller.vueDynamicScroller.vueDynamicScrollerItem.vue这三个组件,然而RecycleScroller为实现核心。

在demo上看到有两个不同的实现,他们两者之间的区别是什么呢?在应用上 RecycleScroller 需要item的高度为静态的,也就是列表每个item的高度都是一致的。而 DynamicScroller就可以兼容item的高度为动态的。但是理论上 RecycleScroller 也可以实现动态高度的item,只要有方案计算到item的height就可以(DynamicScrollerItem解决的就是这个问题)。

分析一个js库,我们需要从目录下的 package.json 分析对应的入口或者打包配置文件。

从目录 ./build 我们可以到对应的打包脚本,入口是 ./src/index.js。打开 index.js 可以看到这个插件是以注册全局组件插件的方式实现相应需求。

function registerComponents (Vue, prefix) {
  Vue.component(`${prefix}recycle-scroller`, RecycleScroller)
  Vue.component(`${prefix}RecycleScroller`, RecycleScroller)
  Vue.component(`${prefix}dynamic-scroller`, DynamicScroller)
  Vue.component(`${prefix}DynamicScroller`, DynamicScroller)
  Vue.component(`${prefix}dynamic-scroller-item`, DynamicScrollerItem)
  Vue.component(`${prefix}DynamicScrollerItem`, DynamicScrollerItem)
}

简单分析完 index.js 后,再看 ./src/components/ 目录下的Vue组件文件。

接下来就分析一下作为基底的 RecycleScroller.vue 文件。

<div
  v-observe-visibility="handleVisibilityChange"
  class="vue-recycle-scroller"
  :class="{
    ready,
    'page-mode': pageMode,
    [`direction-${direction}`]: true,
  }"
  @scroll.passive="handleScroll"
>
  <!-- $slots.before -->
  <!-- 列表循环item部分 -->
  <!-- $slots.after -->
</div>

通过代码我们可以看到组件引入了自定义指令 v-observe-visibility, 再看发现是通过 import { ObserveVisibility } from 'vue-observe-visibility' 加载指令。

computed: {
  sizes () {
    if (this.itemSize === null) {
      // ...
      for (let i = 0, l = items.length; i < l; i++) {
        current = items[i][field] || minItemSize
        if (current < computedMinSize) {
          computedMinSize = current
        }
        accumulator += current
        sizes[i] = { accumulator, size: current }
      }
      // ...
      return sizes  // 返回每个item带size(高度)的数组
    }
    return []
  },
},

主要实现原理是通过 this.updateVisibleItems() 计算出startIndex, endIndex获取需要渲染的元素数组。此处还会涉及的一个视口的计算,相应值为 scroll: {start: xxx, end: xxx}. 通过视口的可视范围计算出 startIndex,...endIndex 每个元素的对应样式值,这里是通过css3的transform控制元素显示。代码是

style="ready ? { transform: `translate${direction === 'vertical' ? 'Y' : 'X'}(${view.position}px)` } : null"

就会想,长列表那么多数据,demo都生成1W+了,每个滚动都去遍历一次,那算法复杂度不就很高了吗。所以这里就需要相关的算法知识。

在计算过程中,使用了 二分法 提高程序的执行效率。

// Searching for startIndex
do {
  oldI = i
  h = sizes[i].accumulator
  if (h < scroll.start) {
    a = i
  } else if (i < count - 1 && sizes[i + 1].accumulator > scroll.start) {
    b = i
  }
  i = ~~((a + b) / 2)
} while (i !== oldI)
i < 0 && (i = 0)
startIndex = i

为什么可以使用这个呢。在computed: sizes[]的时候就已经将item以 accumulator 从小到大排好序了。所以 this.sizes 是一个有序的列表。

分析完 RecycleScroller.vue 接下来就理解DynamicScroller.vueDynamicScrollerItem.vue的代码。

看代码我们可以发现 DynamicScroller 的实现也依赖了 RecycleScroller.vue。 是通过 DynamicScrollerItem.vue 实现获取每个item的height/width得到数组元素对应的size,再回归到 RecycleScroller.vue 的相应实现。

整体下来大概是这样一个情况,总结得有点乱。希望大家一起进步。

对于源代码很多人都抗拒,但这真的是一种很有效学习方式。