100行代码不到,两种方式实现虚拟列表~粗暴!

198 阅读3分钟

**

直接先上最终效果图以及代码~

从1拉到10w,顺畅无比~

最终效果图.gif

<template>
  <div class="outBox">
    <div class="innerBox" :style="{paddingTop:`${firstItemIndex * 50}px`,paddingBottom:`${(data.length - pageSize - firstItemIndex) * 50}px`,transform:`translateY(${ firstItemIndex >= surplus ? `${surplus * -50}px` : `${firstItemIndex * -50}px`})`}">
      <div class="text-div" v-for="item of listArr">{{item}}</div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      firstItemIndex:0, //当前渲染的list第一条数据在原数据的index
      data:[],//致死量数据
      pageSize:10, //一页显示的条数
      surplus:4  //上下多显示的条数
    };
  },
  created() {
    for(let i = 0; i < 100000; i++){
      this.data.push(`致死量数据${i}`)
    }
  },
  mounted() {
    this.canSee()
  },
  computed:{
    //真正渲染的数据
    listArr:function (){
      let start,end
      //开始项,如果第一项比多显示的条数多直接减去,否则就直接拿0就好
      if(this.firstItemIndex >= this.surplus){
        start = this.firstItemIndex - this.surplus
      }else{
        start = 0
      }
      //最后项直接把需要多显示的条数加上
      end = this.firstItemIndex + this.pageSize + this.surplus
      return this.data.slice(start,end)
    }
  },
  methods: {
    changeFirstItemIndex(scrollTop){
      this.firstItemIndex = Math.round(scrollTop/50)
      //因为上下会多渲染surplus,firstItemIndex有可能超出本身长度,加多个超出判断
      if(this.firstItemIndex > this.data.length - this.pageSize) this.firstItemIndex = this.data.length - this.pageSize
    },
    canSee(){
      const outBox = document.querySelector('.outBox')
      const domAll = document.querySelectorAll('.text-div')

      //方式一:这个是使用IntersectionObserver实现,使用滚动条直接快速拉底会出现空白
      const io = new IntersectionObserver(entries =>{ if(entries.length > 0) this.changeFirstItemIndex(outBox.scrollTop)})

      for(let i = 0; i < domAll.length; i++){
        io.observe(domAll[i])
      }

      //方式二:这个是使用onsroll事件实现,不加防抖完美,加防抖快速滑动会闪一下
      outBox.onscroll=() => {this.changeFirstItemIndex(outBox.scrollTop)}
    }
  }
};
</script>

<style scoped>
.outBox{
  height: 500px;
  overflow: auto;
  border:2px solid blue;
  box-sizing: border-box
}
.innerBox{
  height: 500px;
  box-sizing: border-box;
}
.text-div{
  box-sizing: border-box;
  height: 50px;
  border-bottom:2px solid red;
  background:blueviolet;
  color:#fff;
}
</style>

假设现在有10w条数据,假设,我是说假设阿,不要去为难后端同学。这10w条数据需要渲染展示,如果直接将10w条数据渲染到dom,渲染的时候浏览器就会出现非常明显的卡顿,用户体验不好,性能拉跨。所以我们可以选择虚拟列表来实现。大白话讲就是只渲染用户看到的东西,用户看不到的东西不渲染出来。(如下图,用户在页面看到的只是红色框框里的div,红色框框外面的用户是看不到,没有必要渲染出来。)

用户看不到的东西不一定是鬼。。。

11.png

介绍完了虚拟列表就直接开始着手实现一个虚拟列表吧~,第一种实现方式是用IntersectionObserver实现的,不了解IntersectionObserver的可以了解下下,不细讲,不是本篇重点。开始~

一共三层,一个outBox包含innerBox,最里面就是数据列表的展示。

<div class="outBox">
  <div class="innerBox">
    <div class="text-div" v-for="item of listArr">{{item}}</div>
  </div>
</div>

样式

<style scoped>
.outBox{
  height: 500px;
  overflow: auto;
  border:2px solid blue;
  box-sizing: border-box
}
.innerBox{
  height: 500px;
  box-sizing: border-box;
}
.text-div{
  box-sizing: border-box;
  height: 50px;
  border-bottom:2px solid red;
  background:blueviolet;
  color:#fff;
}
</style>

三个变量。data是10W条数据的数组。firstItemIndex是当前渲染的list第一条数据在原数据的索引。比如现在用户看到的是第104条到114条,那么firstItemIndex就是104,pageSize是实际渲染的条数。

firstItemIndex:0, //当前渲染的list第一条数据在原数据的index
data:[],//致死量数据
pageSize:10, //渲染的条数

生成致死量数据。

for(let i = 0; i < 100000; i++){
  this.data.push(`致死量数据${i}`)
}

canSee方法是通过IntersectionObserver监听到用户浏览的数据的变化。比方用户从第10条滚到第13条,那么就会触发我们通过IntersectionObserver给每一条绑定的事件,然后触发了changeFirstItemIndex方法,changeFirstItemIndex就是通过当前outBox的滚动高度去获取到当前渲染的list第一条数据在原数据的索引。

changeFirstItemIndex(scrollTop){
  this.firstItemIndex = Math.round(scrollTop/50)
},
canSee(){
  const outBox = document.querySelector('.outBox')
  const domAll = document.querySelectorAll('.text-div')
  const io = new IntersectionObserver(entries =>{ if(entries.length > 0) this.changeFirstItemIndex(outBox.scrollTop)})

  for(let i = 0; i < domAll.length; i++){
    io.observe(domAll[i])
  }
}

因为我们只需要渲染pageSize条数的数据,所以需要有一个“虚拟滚动”,否则用户只能看到10条数据。虚拟滚动我是用padding实现的。拿上面第一张图说,红色圈上面的要用padding-top撑下来,撑下来的高度是firstItemIndex * 每一条的高度,这里是50px。红色圈下面的要用padding-bottom撑下去,撑下去的高度是原始数据量减去当前已经渲染的(即pageSize),再减去firstItemIndex。然后再去乘50px。

<div class="outBox">
  <div class="innerBox" :style="{paddingTop:`${firstItemIndex *  50}px`,paddingBottom:`${(data.length - pageSize - firstItemIndex) * 50}px`}">
    <div class="text-div" v-for="item of listArr">{{item}}</div>
  </div>
</div>

真正渲染的数据,我是vue写的,用的计算属性computed。(从原始数据截取从firstItemIndex开始,然后pageSize条数据)

  computed:{
    listArr:function (){
      return this.data.slice(this.firstItemIndex,this.firstItemIndex + this.pageSize)
    }
  },

到这里就可以看到效果了。

效果1.gif

然后发现往下滚动的时候,下面会出现空白区域。产生的原因就是,因为我们每一条高度是50px,当滚动了低于50px的距离时,是不会触发IntersectionObserver的,用户看到的也还是那10条数据。IntersectionObserver就是元素离开或者出现的时候会触发。空白区域其实就是我们滚动的高度低于条数高的时候的距离。

知道问题产生的原因后就是解决出问题的人了,居然是我自己,那算了。

1638937811568.jpg

解决方案是多给它渲染几条用户看不见的数据就是了。如下图(蓝色框框就是我们多渲染的,往上滚和往下滚都需要)

22.png

多定义一个变量

surplus:4  //上下多显示的条数

然后修改changeFirstItemIndex方法。

changeFirstItemIndex(scrollTop){
  this.firstItemIndex = Math.round(scrollTop/50)
  //因为上下会多渲染surplus条,firstItemIndex有可能超出本身长度,加多个超出判断
  if(this.firstItemIndex > this.data.length - this.pageSize) this.firstItemIndex = this.data.length - this.pageSize
},

修改computed

computed:{
  //真正渲染的数据
  listArr:function (){
    let start,end
    //开始项,如果第一项比多显示的条数多直接减去,否则就直接拿0就好
    if(this.firstItemIndex >= this.surplus){
      start = this.firstItemIndex - this.surplus
    }else{
      start = 0
    }
    //最后项直接把需要多显示的条数加上
    end = this.firstItemIndex + this.pageSize + this.surplus
    return this.data.slice(start,end)
  }
},

因为多渲染了4条看不见的数据,所以innerBox需要往下偏移,否则就会出现用户应该看到的是第5条,然后因为上下多渲染了4条,用户实际看到的却是第1条,这太离谱。

小问题,直接用translateY解决。

`transform:`translateY(${ firstItemIndex >= surplus ? `${surplus * -50}px` : `${firstItemIndex * -50}px`

然后看一下效果,好像确实应该可能也许是差不多完美了

效果1.gif

然后.....当猛拉滚动条的时候,前途突然一切光芒

效果3.gif

这是因为数据量很大,WORD很大,忍一下。如果壮汉猛拉滚动条,会跳过很多我们通过IntersectionObserver绑定的事件,(比如从1直接滚到5W),如果是移动端不用考虑这个问题,因为移动端壮汉都没法一次性拉跨度那么大,手机屏幕也不允许壮汉这么做。但是怎么解决呢?我也不知道。。。所以,我不用IntersectionObserver实现了,直接用onscroll实现。简单粗暴,直接把方式一换成

//方式二:这个是使用onsroll事件实现,不加防抖完美,加防抖快速滑动会闪一下
outBox.onscroll=() => {this.changeFirstItemIndex(outBox.scrollTop)}

就ok了,尝试加了防抖,但是如果加上防抖,壮汉猛拉会出现闪一下的问题,因为再怎么防抖都有个时间差。。。所以用户体验贼拉跨,干脆就不加了。

最终效果图.gif

至此,一个自我感觉还算不错的虚拟列表就诞生了。

工作两年多了,第一次自己决心写点东西。确切说,是第一次付出行动。之前都一直停留在想写但没动笔,不是没时间,而是实在懒,希望以后能多动手写写,也算个人的一个成长记录吧。