虚拟长列表设计思路及简单实现

1,972 阅读4分钟

前前言

Hello,大家好。 我是来自推啊前端团队的D同学。 今天跟大家简要分享一下虚拟长列表的相关内容。 效果图

03.gif

1. 设计思路

虚拟列表是按需显示的一种技术,在相似条目非常多的场景下,只渲染可视区域内及周边不多的元素,然后根据用户的滚动来复用Dom元素,属于一种性能优化的手段。

主要的设计思路是:

  • 利用可视区之外的Dom元素的复用
  • 给容器加上Padding撑开尺寸
  • 令可视区之外的区域仅仅是撑开的padding空白,而只需要渲染可视区域和上下几个内容区(为了避免数据过多而产生卡顿,需要提前和滞后渲染相应数据)。

简单Dom结构

<div class="container">
    <ul>
      <li>2020</li>
      <li>2019</li>
      <li>2018</li>
      <li>2017</li>
      <li>2016</li>
      <li>2015</li>
      <li>2014</li>
    </ul>
</div>

接着,在使用JavaScript控制行为之前,需要首先考虑一下临界状态,以及临界状态后的大致行为。

如图

01.png

  • 在下拉至设定阈值之后,将第一个元素追加到容器内,然后容器加上poddingTop,阈值变化。
  • 在上拉至设定阈值之后,把最后一个元素插入到第一个元素之前,减少容器paddingTop,阈值变化。

本次实例中,下拉的初始阈值即是三个lI的高度,上拉的初始阈值为0 。

同时,容器顶部的padding值是由li的变化而动态计算了,因此还要保存一个变量,来计算容器的paddingTop。

2. 主要代码实现

let ul = document.getElementsByTagName('ul')[0];
let item = document.getElementsByTagName('li')[0];
// 单个item的高度
const ItemWidth = parseInt(getComputedStyle(item).height)
let obj = {
  initx: 0,
  // 控制mousemove的移动与否
  isMove: false,
  // 初始最大值,用来判断上拉时的界限
  max: 0,
  // 初始最小值,用来判断下拉时的界限
  min: -3 * ItemWidth,
  topIndex: 0
}
// 不严谨的获得元素Y轴上的偏移值 
function getTransY(el) {
  if (!el.style.transform) {
    return 0
  }
  let str = el.style.transform.replace(/.*?,/, '')
  return parseInt(str)
}

之后,按部就班的对容器进行鼠标事件监听,实现拖动效果。

ul.addEventListener('mousedown', (e) => {
  // 这里记录一下鼠标的坐标,为了计算偏移量
  // 记录ul的初始偏移值,是为了加上偏移量,算出总的偏移值
  obj.isMove = true,
  obj.initY = e.clientY
  obj.initTrans = getTransY(ul)
})
ul.addEventListener('mousemove', (e) => {
    if(obj.isMove){
      // 原有偏移值 + 鼠标滑动距离
      let abs = obj.initTrans + (e.clientY - obj.initY) 
      // 边界判断,不能小于初始状态0
      abs = abs > 0 ? 0 : abs
      ul.style.transform = `translate3d(0,${abs}px,0)`
      if (abs < obj.min) {
        obj.max -=  ItemWidth
        obj.min -=  ItemWidth
        // changeStatus(0)
        console.log('append')
      } else if (abs > obj.max) {
        obj.max +=  ItemWidth
        obj.min +=  ItemWidth
        // changeStatus(1)
        console.log('insertBefore')
    }
  }
})
ul.addEventListener('mouseup', () => {
  obj.isMove = false
})

具体的追加和插入代码先放下,测试一下逻辑上有没有问题。

  • 追加

01.gif

  • 插入

02.gif

可以清晰的看出,在逻辑上是跑得通的,接下来的事情就是对ul内的元素进行操作了。

我们把这个复杂的逻辑提取出来,通过传参来实现追加和插入两种效果。

function changeStatus(flag) {
  let lgh = ul.childElementCount
  // 获得第一个元素
  let fir = ul.children[0]
  // 获取最后一个元素
  let las = ul.children[lgh - 1]
  if (flag) {
    // insertBefore
    // 这里理论上应该是内容如何变化,这里简单改变了一下值
    las.innerText = +fir.innerText + 1
    // 这里用于计算paddingTop值的索引减一
    obj.topIndex--
    // 每插入一个元素,则ul的paddingTop占位值减一个item的高度 
    ul.style.paddingTop = obj.topIndex * ItemWidth + 'px'
    ul.insertBefore(las, fir)
  } else {
    // append
    // 每将第一个元素追加至最后位置,则ul的paddingTop占位值加一个item的高度 
    obj.topIndex++
    ul.style.paddingTop = obj.topIndex * ItemWidth + 'px'
    fir.innerText = +las.innerText - 1
    ul.appendChild(fir)
  }
}

这样处理完完毕,在把ul所在容器的overflow:hidden加上,就可以简单模拟出一个虚拟无限长列表的视觉效果了。

基于该思路,可以简单实现一个虚拟长列表日期选择器。

04.gif

结语

本文只是简单的介绍一些虚拟长列表的简单实现方式,真是要完成一个比较完整的虚拟长列表的话,还有诸多细节需要去挖掘实现。
不过万变不离其宗,核心即是复用Dom去填充新数据。

比如暴露出自定义配置项,实现纵向还是横向滚动。

或者在非无限滚动的场景下,给定一个拟态滚动条,用来模拟出真实滚动条效果,并且能够让用户大致明白内容的篇幅。

投稿来自「虚拟长列表」