背景:
对于长列表的渲染,个别是才采纳分页或者懒加载的形式,下拉到底部又向后端申请数据,每次只加载一部分数据,然而随着加载的数据越来越多。页面的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...