乐趣区

关于前端:长列表渲染优化虚拟列表

背景:

对于长列表的渲染,个别是才采纳分页或者懒加载的形式,下拉到底部又向后端申请数据,每次只加载一部分数据,然而随着加载的数据越来越多。页面的 Dom 在有限减少中,给浏览器带来累赘,整个滑动也会呈现卡顿。

解决方案:虚构列表

虚构列表其实是 按需显示 的一种体现。只对可视区进行渲染,对于非可视区数据不渲染或局部渲染,加重浏览器累赘,晋升渲染性能。

对于首次渲染,可依据可视区高度 ÷ 单个列表项高度 = 一屏须要渲染的列表个数。
当滚动产生时,记录滚动间隔,依据滚动间隔和单个列表项高度,可晓得以后可视区域开始索引。同时,为了营造出滚动成果,列表区域,设置 transform 属性的 translate 的 Y 值为 scrollTop – (scrollTop % itemSize)(当滚动到某数据项的两头时,transform 的 y 值不包含该数据项)

总结:虚构列表的实现,实际上就是在首屏加载的时候,只加载可视区域内须要的列表项,当滚动产生时,动静通过计算取得可视区域内的列表项,并将非可视区域内存在的列表项删除。Dom 不变,数据扭转。躲避了分页和懒加载会让 Dom 有限减少的毛病。

两种场景的具体实现:

1. 定高场景

(1)首先是确定 DOM 构造:

第一层作为 container,作为容器层。作用:监听滚动,记录滚动地位 scrollTop
第二层分为占位层和列表层,两者是并列关系,占位层的次要作用是依据理论整体列表长度进行占位,用于造成滚动条。列表层就是可视化区域,渲染列表区域,用 translate3d 展现动画滚动成果,其中 y 值与容器层记录滚动地位无关。

(2)父组件传入所有列表数据,以及每个列表项的高度。(3)能够计算出整个列表长度,为占位层高度赋值。数据长度 * 单个列表项高度(4)计算可视区域高度,推算出一屏可显示列表个数。定义 start、end 两个变量用于管制可视区的开始索引和完结索引。通过 start、end 索引更新可视区列表数据。(5)监听 container 滚动,记录滚动地位 scrollTop,同时更新 start、end,以及列表区域的偏移量 scrollTop - (scrollTop % 单个列表项高度)
<template>
  <div class="container" ref="list" @scroll="handleScroll()">
    <div class="phantom" :style="{height: listHeight +'px'}"></div>
    <div class="list" :style="{transform: getTransform}">
      <div
        ref="items"
        class="list-item"
        v-for="item in visibleData"
        :key="item.id"
        :style="{height: itemSize +'px', lineHeight: itemSize +'px'}"
      >
        {{item.value}}
      </div>
    </div>
  </div>
</template>
<script>
// 须要接管 listData 以及每个列表项的高度
export default {
  name: "VirtualList",
  props: {
    listData: {
      type: Array,
      default: () => [],
    },
    itemSize: {
      type: Number,
      default: 200,
    },
  },
  data() {
    // 应用 return 是因为一个组件能够被屡次实例化,data 如果是对象模式,则该组件所有实例的 data 都指向同一地址,一个实例对 data 的批改会影响所有实例。return {
      // 可视区域高度
      screenHeight: 0,
      // 偏移量
      startOffset: 0,
      // 开始索引
      start: 0,
      // 完结索引
      end: null,
    };
  },
  computed: {
    // 列表总高度
    listHeight() {return this.listData.length * this.itemSize;},
    // 可显示的列表数目
    visibleCount() {
      // Math.ceil 向上取整
      return Math.ceil(this.screenHeight / this.itemSize);
    },
    // 获取渲染区数据
    visibleData() {
      // 兼容数据有余一屏的状况
      return this.listData.slice(
        this.start,
        Math.min(this.end, this.listData.length)
      );
    },
    // 偏移量对应的 style
    getTransform() {return `translate3d(0,${this.startOffset}px,0)`;
    },
  },
  mounted() {
    this.screenHeight = this.$el.clientHeight;
    this.start = 0;
    this.end = this.start + this.visibleCount;
  },

  methods: {
    // 监听 scroll,获取滚动地位 scrollTop
    handleScroll() {
      let scrollTop = this.$refs.list.scrollTop;
      this.start = Math.floor(scrollTop / this.itemSize);
      this.end = this.start + this.visibleCount;
      this.startOffset = scrollTop - (scrollTop % this.itemSize);
      console.log("scrollTop", scrollTop);
      console.log("startOffset", this.startOffset);
    },
  },
};
</script>
<style scoped>
.container {
  width: 100vw;
  height: 100%;
  overflow: auto;
  position: relative;
}
.phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}
.list {
  left: 0;
  right: 0;
  top: 0;
  position: absolute;
  text-align: center;
}
.list-item {
  padding: 10px;
  box-sizing: border-box;
  border-bottom: 1px solid black;
}
</style>

2. 不定高场景
之前的定高场景,能够依据可视区的高度以及单个列表项的高度,准确算出须要渲染的列表数目。然而理论利用中,很多列表项的高度可能不固定。在虚构列表中解决不定高状况的计划个别有三种:

(1)扩大组件的 itemSize 属性,反对的类型能够为数字、数组、函数。然而前提是须要晓得每项列表的高度;(2)将列表项渲染到可视区外,对其高度进行测量缓存,而后再将其渲染到可视区域。但渲染老本进步一倍,不可行;(3)应用预估高度。在更新页面时,记录每个列表项的实在高度以及地位信息。

因为第一种和第二种计划可行度不高,这里采纳第三种计划。

  • 定义组件属性 estimatedItemSize,用于接管预估高度;
  • 定义 position,用于列表项渲染后存储每一项的高度以及地位信息;
  • 对 position 进行初始化;有 index、height、top、bottom 值;
initPositions() {this.positions = this.listData.map((item, index) => {
        return {
            index,
            height: this.estimatedItemSize,
            top: index * this.estimatedItemSize,
            bottom: (index + 1) * this.estimatedItemSize        
        }    
    })
}
  • 计算占位层高度
listHeight() {return this.position[this.positions.length - 1].bottom;
}
  • 渲染实现后,在 update 获取每项列表的高度以及地位信息,存储到 positions 外面;
updated() {
    let nodes = this.$refs.items;
    nodes.forEach(node => {let rect = node.getBoundingClientRect();
        let height = rect.height;
        let index = +node.id.slice(1);
        let oldHeight = this.positions[index].height;
        // 计算预估高度与理论高度的差值
        let dValue = oldHeight - height;
        if(dValue !== 0) {
            // 更新该元素的 height 和 bottom
            this.positions[index].height = height;
            this.positions[index].bottom = this.positions[index].bottom - dValue;
            // 因为 height 扭转,须要更新该元素前面的 top、bottom;
            for (let k = index + 1; k < this.positions.length; k++) {this.positions[k].top = this.positions[k-1].bottom;
                this.positions[k].bottom = this.positions[k].bottom - dValue;            
            }                    
        }    
    })
}
  • 滚动后获取开始索引,因为缓存数据是有程序的,通过二分法获取, 找到最迫近 scrollTop 的列表项。计算是参考每个列表项地位信息中的 Bottom;
getStartIndex(scrollTop = 0) {return this.binarySearch(this.positions, scrollTop);
}
// 二分查找
// 因为间隔很少可能性找到一个齐全精确的值。所以在 middleValue > Value 这种状况下用一个 tempIndex 去记录。end 往左挪动一位。返回 tempIndex 的值。binarySearch(list, value) {
    let start = 0;
    let end = list.length - 1;
    let tempIndex = null;
    while(start <= end) {let middle = start + Math.floor(end - start);
        let middleValue = list[middle].bottom;
        if (middleValue === value) {
            // 因为是以 bottom 作为参照,返回的是列表开始索引,须要 +1
            return    middle + 1;    
        } else if (middleValue < value) {start = middle + 1} else {if (iempIndex === null || tempIndex > midIndex) {tempIndex = middleIndex;}
            end = end - 1;     
        }    
    }
    return tempIndex;
},
  • 滚动后将偏移量的获取形式变更
scrollEvent() {
    // ....
    if (this.start >= 1) {this.startOffset = this.positions[this.start - 1].bottom;
    } else {this.startOffset = 0;}
}

其余计划:

当初的长列表优化曾经有较为成熟的解决方案,在 react 中 react-virtualized 以及 react-window 都绝对比拟优良。他们的外围办法还是虚构列表。

react-virtualized: https://www.jianshu.com/p/fc9…

利用所提供的 List 组件,设置组件的宽高,渲染总数量 rowCount, 每个列表卡片的高度 rowHeight, 以及每个列表卡片的渲染函数 rowRende。

参考文献:
https://juejin.cn/post/684490…

退出移动版