不是,是谁还在傻傻遍历生成成千上万个DOM?

虚拟列表赶紧用起来,轻轻松松解决超多重复DOM节点造成的卡顿~

以下有三种不同级别的虚拟列表,分别针对生成的重复DOM节点是固定高度、不同高度和动态变化高度~

1.基础段位:固定高度

虚拟列表的原理其实就是以下几条:

①一个外层盒子提供滚动事件

②外层盒子中装的第一个是platform,一个空盒子,这个空盒子的高度是列表如果真实渲染应该有的高度,作用是为了撑开外层盒子,提供滚动条

②外层盒子中装的第二个是展示列表盒子,这个盒子中放置所有现在应该出现在页面上的列表项和前后缓冲区。该盒子采用绝对定位,top值根据滚动位置实时改变,让展示列表不论怎么滚动一直出现在页面上

④酌情给一些在页面展示之前之后的缓冲区,防止因为用户滚动过快而造成的空白

vue 复制代码
<template>
  <div class="virtualBox" @scroll="scrollEvent" ref="virtualBox">
      <div class="platform" :style="{ height: platformHeight + 'px' }">
      </div>
      <div class="trueBox" :style="{ top: top + 'px' }">
        <div v-for="(key, value) in showData" class="itemBox" ref="itemBox">
          <button>看起来{{ key }} 其实我是{{ value }}</button>
        </div>
      </div>
    </div>
</template>
​
<script>
export default {
  name: 'WebFront',
  data() {
    return {
      listData: [],//真实列表Data
      count: 100,//真实列表项的个数,我这里为了展示手动赋值,真是使用直接获取Data长度即可
      platformHeight:0//platform的高度
      showData: [],//被展示的列表Data
      startIndex: 0,//开始截取listData的Index
      showNum: 1,//页面高度可以展示几个列表项
      top: 0,//展示列表盒子绝对定位的top值
      catchFrontNum: 4, //前缓冲区的数量
      catchBackNum: 4,//后缓冲区的数量
      itemHeight: 0,//列表项的高度
    }
  },
  methods: {
    scrollEvent(e) {
      let scrollTop = e.target.scrollTop//获取滚动的距离
      this.startIndex = Math.ceil(scrollTop / this.itemHeight)//滚动距离除以列表项的高度得到应该展示的列表项Index
      this.startIndex =
        this.startIndex < this.catchFrontNum
          ? 0
          : this.startIndex - this.catchFrontNum//设置前缓冲区
        //对展示的数组进行截取
        this.showData = this.listData.slice(
        this.startIndex,
        this.startIndex + this.showNum + this.catchBackNum + this.catchFrontNum
      )
      //绝对定位的展示列表项盒子的top值
      this.top = this.startIndex * this.itemHeight
    },
  },
  mounted() {
    const virtualBox = this.$refs.virtualBox // 获取到最外层盒子
    let itemBox = document.getElementsByClassName('itemBox')[0]
    this.itemHeight = itemBox.offsetHeight//获取列表项
    this.platformHeight = this.count * this.itemHeight
    this.showNum = Math.ceil(virtualBox.clientHeight / this.itemHeight)//外层盒子的可视高度除以列表项高度可以得到展示数量
    this.showData = this.listData.slice(
      this.startIndex,
      this.startIndex + this.showNum + this.catchBackNum+ this.catchFrontNum
    )
  },
  created() {
    //做一些假数据用于展示
    let i = 0
    for (i = 0; i < 100; i++) {
      this.listData[i] = '我是' + i
    }
    this.showData = this.listData.slice(0, 20)
  },
}
</script>
<style scoped>
.trueBox {
  position: absolute;
  top: 0;
}
.itemBox {
  height: 50px;
  background-color: green;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 10px;
}
.platform {
  background-color: red;
}
.virtualBox {
  height: 85vh;
  overflow: scroll;
  position: relative;
}
</style>

2.进阶段位:不同高度

与固定高度不同,列表项的高度是不固定的,所以会出现以下这些难点:

①无法通过页面高度除以列表项高度得到应当展示的数量,也就是展示列表的长度

②无法通过滚动了的高度scrollTop除以列表项高度得到此时应该展示的列表项Index

③无法直接通过ListData的长度乘以列表项高度得到platform的高度

对于以上难点我们的解决方案:

①设置一个预告高度,用于计算页面展示的数量,该预估高度建议偏小,避免出现页面展示数量不够的情况

②设置一个position数组,计算并存储每一个列表项的top\bottom\height值,通过比较scrollTop和列表项的position可以得到此时应该展示的列表项Index

③通过position数组获取最后一个列表项的bottom值,即为platform的高度

vue 复制代码
<template>
  <div class="virtualBox" @scroll="scrollEvent" ref="virtualBox">
      <div class="platform" :style="{ height: platformHeight + 'px' }">
        <!-- 这是假的容器,作用:撑开盒子和提供滚动效果 -->
      </div>
      <div class="trueBox" :style="{ top: top + 'px' }">
        <div
          v-for="(item, key) in showData"
          class="itemBox"
          ref="items"
          :id="item.id"
          :key="item.id"
        >
          看着第{{ key }}个 其实第{{ item.id }}个
          {{ item.value }}
        </div>
      </div>
    </div>
</template>
​
<script>
export default {
  name: 'WebFront',
  data() {
    return {
      position: [],
      listData: [],
      platformHeight: 0,
      count: 100,
      scrollTop: 0,
      showData: [],
      startIndex: 0,
      showNum: 0,
      top: 0,
      estimatedItemHeight: 100,//预设高度
    }
  },
  methods: {
    updateItemsSize() {
        //更新列表项高度
      let nodes = this.$refs.items
      nodes.forEach((node) => {
        let rect = node.getBoundingClientRect()
        let height = rect.height
        let index = parseInt(node.id)
        let oldHeight = this.position[index].height
        let dValue = oldHeight - height
        if (dValue) {
          this.position[index].bottom = this.position[index].bottom - dValue
          this.position[index].height = height
          for (let k = index + 1; k < this.position.length; k++) {
            this.position[k].top = this.position[k - 1].bottom
            this.position[k].bottom = this.position[k].bottom - dValue
          }
          this.platformHeight = this.position[this.position.length - 1].bottom
        }
      })
    },
    findStartIndex(scrollTop, list) {
        //根据滚动高度scrollTop找到此时的startIndex
      for (let i = 0, len = list.length; i < len; i++) {
        if (list[i].top > scrollTop) {
          return i - 1
        }
      }
      return list.length - 1
    },
    scrollEvent(e) {
      this.updateItemsSize()
      this.scrollTop = e.target.scrollTop
      let index = this.findStartIndex(this.scrollTop, this.position)
      this.startIndex =
        index < this.listData.length - 1 - this.showNum
          ? index
          : this.listData.length - 1 - this.showNum
        //至少保留showNum个列表项
      this.showData = this.listData.slice(
        this.startIndex,
        this.startIndex + this.showNum + 2
      )
      this.top = this.position[this.startIndex].top
    },
    createString(num) {
      let str = ''
      for (let i = 0; i < num; i++) {
        str += 'aa'
      }
      return str
    },
  },
  mounted() {
    this.position = this.listData.map((item, index) => ({
      index,
      top: index * this.estimatedItemHeight,
      bottom: (index + 1) * this.estimatedItemHeight,
      height: this.estimatedItemHeight,
    }))
    this.platformHeight = this.position[this.position.length - 1].bottom
    this.showNum = Math.ceil(
      this.$refs.virtualBox.clientHeight / this.estimatedItemHeight
    )
    this.showData = this.listData.slice(
      this.startIndex,
      this.startIndex + this.showNum + 2
    )
  },
  created() {
    let i = 0
    for (i = 0; i < 100; i++) {
      this.listData[i] = {}
      this.listData[i].value = this.createString(
        Math.floor(Math.random() * 100)
      )
      this.listData[i].id = i
    }
    this.showData = this.listData.slice(0, 20)
  },
}
</script>
<style scoped>
.trueBox {
  position: absolute;
  top: 0;
}
.itemBox {
  background-color: green;
  display: block;
  line-height: 100%;
  word-break: break-all;
  width: 100px;
  padding: 10px;
  border: 2px purple solid;
}
.platform {
  background-color: red;
}
.virtualBox {
  height: 85vh;
  overflow: scroll;
  position: relative;
}
</style>
​

3.高阶段位:变化高度

这种情况可能出现在比如列表项因为太长而设置了展开/收缩按钮,此时列表项的高度是动态发生变化的,这种情况和上一种情况差不多,区别只在于这种情况只需要在点击按钮的时候将position更新即可~所以在这里不做代码演示啦

总结

了解了原理要写出来还是不难的~但我个人感觉有的时候前端的进阶难就难在止步于此,现在的浏览器性能好,可能写N个DOM都不会卡顿,很难会有觉悟自己去写一个虚拟列表来试试看性能是不是更好。希望自己永不止步,永远进步

相关推荐
it_remember1 小时前
新建一个reactnative 0.72.0的项目
javascript·react native·react.js
敲代码的小吉米2 小时前
前端上传el-upload、原生input本地文件pdf格式(纯前端预览本地文件不走后端接口)
前端·javascript·pdf·状态模式
da-peng-song3 小时前
ArcGIS Desktop使用入门(二)常用工具条——数据框工具(旋转视图)
开发语言·javascript·arcgis
低代码布道师4 小时前
第五部分:第一节 - Node.js 简介与环境:让 JavaScript 走进厨房
开发语言·javascript·node.js
满怀10154 小时前
【Vue 3全栈实战】从响应式原理到企业级架构设计
前端·javascript·vue.js·vue
伟笑5 小时前
elementUI 循环出来的表单,怎么做表单校验?
前端·javascript·elementui
确实菜,真的爱5 小时前
electron进程通信
前端·javascript·electron
魔术师ID7 小时前
vue 指令
前端·javascript·vue.js
Clown957 小时前
Go语言爬虫系列教程 实战项目JS逆向实现CSDN文章导出教程
javascript·爬虫·golang
星空寻流年8 小时前
css3基于伸缩盒模型生成一个小案例
javascript·css·css3