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

发布于:2024-04-26 ⋅ 阅读:(26) ⋅ 点赞:(0)

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

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

1.基础段位:固定高度

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

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

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

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

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

<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.进阶段位:不同高度

tutieshi_640x657_7s.gif

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

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

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

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

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

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

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

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

<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都不会卡顿,很难会有觉悟自己去写一个虚拟列表来试试看性能是不是更好。希望自己永不止步,永远进步