深入【虚拟列表】动态高度、缓冲、异步加载... Vue实现

news2024/10/3 4:32:55

前言🎀

在前文中我们了解到:

1.在某种特殊场景下,我们需要将 大量数据 使用不分页的方式渲染到列表上,这种列表叫做长列表
2.因为事件循环的机制,一次性大量的渲染耗时较长,并且渲染期间会阻塞页面交互事件,所以我们使用时间分片机制将渲染分为多次。
3.分析真实业务场景,将全部数据渲染到列表中是无用且浪费资源的行为,只需要根据用户的视窗进行部分渲染即可,所以使用到虚拟列表技术。

前文中我们根据 “无论滚动到什么位置,浏览器只需要渲染可见区域内的节点” 的思路实现了虚拟列表解决了长列表问题,但在一些细节和特殊情况的处理上还是有所欠缺,例如:

1.高度不定的列表项会导致内容出现错位、偏移等情况。
2.列表项含有异步资源,会在渲染后再次改变高度。
3.一次性大量数据的请求导致请求响应与数据处理时间过长。

在本文中我们就来一起研究这些场景,并对原版的虚拟列表做出优化 🚀

如果觉得有收获还望大家点赞、收藏 🌹

动态高度

分析

在前文的虚拟列表实现中,列表项高度itemSize都是固定的。

// template -> list-item
:style="{ height: itemSize + 'px', lineHeight: itemSize + 'px' }"

// export defualt -> data
itemSize: 50, 

因此很多直接与 列表项高度itemSize 关联的属性,都很容易计算:

1.列表总高度listHeight= listData.length * itemSize
2.当前窗口偏移量currentOffset= scrollTop - (scrollTop % itemSize)
3.列表数据的开始/结束索引start/end= ~~(scrollTop / itemSize). . . . . .

但在实际情况中列表元素多为高度不固定的列表项,它可能是多行文本、图片之类的可变内容,如系统日志、微博等等。

不固定的高度会导致上述的属性无法正常计算。

对于高度不固定的列表项,我们遇到的问题如下:

1.如何获取真实高度?
2.相关的属性该如何计算?
3.列表渲染的方式有何改变?

方案

如何获取真实高度?

  • 如果能获得列表项高度数组,真实高度问题就很好解决。但在实际渲染之前是很难拿到每一项的真实高度的,所以我们采用预估一个高度渲染出真实DOM,再根据DOM的实际情况去设置真实高度。* 创建一个缓存列表,其中列表项字段为 索引、高度与定位,并预估列表项高度用于初始化缓存列表。在渲染后根据DOM实际情况更新缓存列表相关的属性该如何计算?

  • 显然以前的计算方式都无法使用了,因为那都是针对固定值设计的。* 于是我们根据缓存列表重写 计算属性、滚动回调函数,例如列表总高度的计算可以使用缓存列表最后一项的定位字段的值。列表渲染的方式有何改变?

  • 因为用于渲染页面元素的数据是根据 开始/结束索引 在 数据列表 中筛选出来的,所以只要保证索引的正确计算,那么渲染方式是无需变化的。* 对于开始索引,我们将原先的计算公式改为:在 缓存列表 中搜索第一个底部定位大于 列表垂直偏移量 的项并返回它的索引。* 对于结束索引,它是根据开始索引生成的,无需修改。实现

预估&初始化列表

先设置一个虚拟 预估高度preItemSize,用于列表初始化。

同时维护一个记录真实列表项数据的 缓存列表positions

data() {return {. . . . . .// 预估高度preItemSize: 50,// 缓存列表positions = [// 列表项对象{index: 0,// 对应listData的索引top: 0,// 列表项顶部位置bottom: 50,// 列表项底部位置height: 50,// 列表项高度}]}
} 

在创建组件时先用preItemSizepositions进行初始化,在后续更新时再进行替换。

created() {this.initPositions(this.listData, this.positions)
},
methods: {initPositions(listData, itemSize) {this.positions = listData.map((item, index) => {return{index, top: index * itemSize, bottom: (index + 1) * itemSize,height: itemSize, }})}
} 

注:listData即数据列表,里面是每一项数据对应的内容。

列表总高度listHeight的计算方式改变为缓存列表positions最后一项的bottom

computed: {listHeight() {// return this.listData.length * this.itemSize;
+ return this.positions[this.positions.length - 1].bottom;},
} 

更新真实数据

在每次渲染后,获取真实DOM的高度去替换positions里的预估高度

updated生命周期在数据变化视图更新过后触发所以能获取到真实DOM

我们利用Vue的updated钩子来实现这一功能

期间遍历真实列表的每一个节点,对比 节点 和 列表项 生成高度差dValue判断是否需要更新:

updated() {this.$nextTick(() => {// 根据真实元素大小,修改对应的缓存列表this.updatePositions()})
},
methods: {updatePositions() {let nodes = this.$refs.items;nodes.forEach((node) => {// 获取 真实DOM高度const { height } = node.getBoundingClientRect();// 根据 元素索引 获取 缓存列表对应的列表项const index = +node.idlet oldHeight = this.positions[index].height;// dValue:真实高度与预估高度的差值 决定该列表项是否要更新let dValue = oldHeight - height;// 如果有高度差 !!dValue === trueif(dValue) {// 更新对应列表项的 bottom 和 heightthis.positions[index].bottom = this.positions[index].bottom - dValue;this.positions[index].height = height;// 依次更新positions中后续元素的 top bottomfor(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;}}})}
} 

此外在更新完positions后,当前窗口偏移量currentOffset也要根据真实情况重新赋值:

updated() {this.$nextTick(() => {// 根据真实元素大小,更新对应的缓存列表this.updatePositions()// 更新完缓存列表后,重新赋值偏移量this.currentOffset = this.getCurrentOffset()})
},
methods: {updatePositions() {//. . . }getCurrentOffset() {if(this.start >= 1) {this.currentOffset = this.positions[this.start - 1].bottom} else {this.currentOffset = 0;}}
} 

重写滚动回调

滚动触发的回调函数里计算了 开始/结束索引start/end 和 当前窗口偏移量currentOffset ,现在高度不固定后都需要重新计算,而结束索引依赖于开始索引所以不需要修改。

重新计算 开始索引start

定高时我们不必建立数组(建立了也只是重复的数据),直接根据scrollTopitemSize计算索引即可

this.start = ~~(scrollTop / this.itemSize); 

但不定高时,只能带着scrollTop在列表中逐个寻找(后续使用搜索算法优化)。两个计算的最终目的都是找到当前位置对应的数据索引

列表数据开始索引start的计算方法修改为:遍历 缓存列表positions 匹配第一个大于当前滚动距离scrollTop的项,并返回该项的索引。

mounted() {. . . . . .// 绑定滚动事件let target = this.$refs.virtualListlet scrollFn = (event) => this.scrollEvent(event.target)target.addEventListener("scroll",scrollFn);
},
methods: {scrollEvent(target) {const scrollTop = target.scrollTop;// this.start = ~~(scrollTop / this.itemSize);
+ this.start = this.getStartIndex(scrollTop)this.end = this.start + this.visibleCount;this.currentOffset = scrollTop - (scrollTop % this.itemSize);},getStartIndex(scrollTop = 0) {let item = this.positions.find(item => item && item.bottom > scrollTop); return item.index;}
}, 

重新计算 窗口偏移量currentOffset

滚动后立即根据positions的预估值(此时数据还未更新)计算窗口偏移量currentOffset

scrollEvent() { . . . . . .// this.currentOffset = scrollTop - (scrollTop % this.itemSize);this.currentOffset = this.getCurrentOffset()}, 

优化

positions是遍历listData生成的,listData本是有序的,所以positions也是一个顺序数组

Array.find方法 时间复杂度 O(n)O(n)O(n),查找 索引start 效率较低 ❌

二分查找十分适合顺序存储结构 时间复杂度log2nlog_2{n}log2​n,效率较高 ✔️

<script> . . . . . .
var binarySearch = function(list, target) {    const len = list.length    let left = 0, right = len - 1let tempIndex = null    while (left <= right) {        let midIndex = (left + right) >> 1        let midVal = list[midIndex].bottom        if (midVal === target) {return midIndex} else if (midVal < target) {left = midIndex + 1} else {// list不一定存在与target相等的项,不断收缩右区间,寻找最匹配的项if(tempIndex === null || tempIndex > midIndex) {tempIndex = midIndex}right--}
    }// 如果没有搜索到完全匹配的项 就返回最匹配的项
    return tempIndex};
export default {. . . . . .methods: {. . . . . .getStartIndex(scrollTop = 0) {// let item = this.positions.find(i => i && i.bottom > scrollTop); // return item.index;
+return binarySearch(this.positions, scrollTop)}},
} </script> 

运行查看一下效果,不定高问题已经解决了

滚动缓冲

分析

上文中,为了正确计算不定高列表项,同时在 updated生命周期 和 滚动回调 中增加了额外操作,这都增加了浏览器负担

因此快速滚动列表时,我们很明显的观察到白屏闪烁的情况,即滚动后,先加载出白屏内容(没有渲染)然后迅速替换为表格内容,制造出一种闪烁的现象。

注:白屏闪烁是浏览器性能低导致的,事件循环中的渲染操作没有跟上窗口的滚动,额外操作只是加剧了这种情况。

方案

为了使页面平滑滚动,我们在原先的列表结构上再加入缓冲区,渲染区域由可视区+缓冲区共同组成,这给滚动回调和页面渲染更多处理时间。

用户在可视区滚动,脱离可视区后立即进入缓冲区,同时渲染下一部分可视区的数据。在脱离缓冲区后新的数据大概率也渲染完成了。

而缓冲区域包含多少个元素呢?

我们创建一个变量表示比例数值,这个比例数值是相对于 最大可见列表项数 的,根据这个 相对比例 和 开始/结束索引 计算上下缓冲区的大小

对渲染流程有什么影响?

列表显示数据 原先是根据索引计算,现在额外加入上下缓冲区大小重新计算,会额外渲染缓冲元素。

实现

创建一个属性代表比例值:

data: {bufferPercent: 0.5, // 即每个缓冲区只缓冲 0.5 * 最大可见列表项数 个元素
}, 

创建三个计算属性,分别代表 缓冲区标准多少个元素 + 上下缓冲区实际包含多少个元素:

computed: {bufferCount() {return this.visibleCount * this.bufferPercent >> 0; // 向下取整},// 使用索引和缓冲数量的最小值 避免缓冲不存在或者过多的数据aboveCount() {return Math.min(this.start, this.bufferCount);},belowCount() {return Math.min(this.listData.length - this.end, this.bufferCount);},
} 

重写 列表显示数据visibleData 的计算方式:

computed: {visibleData() {// return this.listData.slice(this.start, this.end);
+return this.listData.slice(this.start - this.aboveCount, this.end + this.belowCount);},
} 

因为多出了缓冲区域所以窗口偏移量currentOffset也要根据缓冲区的内容重新计算:

 getCurrentOffset() {if(this.start >= 1) {// return this.positions[this.start - 1].bottom;let size = this.positions[this.start].top - (this.positions[this.start - this.aboveCount] ? this.positions[this.start - this.aboveCount].top : 0);// 计算偏移量时包括上缓冲区的列表项return this.positions[this.start - 1].bottom - size;} else {return 0;}} 

运行看一下效果,闪烁问题已经完美解决了。

异步加载

其实在列表项中包含图片的场景,图片多为高度固定的缩略图,只需要在计算时根据图给每个列表项加一个固定高度,多于一行的图片直接省略。这样异步加载对于虚拟列表就没有影响了。

 <div v-for="item in visibleData" :key="item.id" :id="item.id" ref="items" class="list-item">{{ item.value }}<img :src="item.img" @load="updatePositions" /></div>
or
mounted() {let content = this.$refs.contentlet resizeObserver = new ResizeObserver(() => this.updatePositions())resizeObserver.observe(content)
}, 

懒加载数据

一次性请求大量数据可能会使后端处理时间增加,过大的响应体也会导致整体请求响应耗时增加,用户等待时间较长体感较差。

因此我们结合懒加载的方式,在每次滚动触底时加载部分新数据并更新positions,避免单次请求等待时间过长。

// 滚动回调scrollEvent(target) {const { scrollTop, scrollHeight, clientHeight } = target;this.start = this.getStartIndex(scrollTop);this.end = this.start + this.visibleCount;this.currentOffset = this.getCurrentOffset()// 触底if ((scrollTop +clientHeight) === scrollHeight) {// 模拟数据请求let len = this.listData.length + 1for (let i = len; i <= len + 100; i++) {this.listData.push({id: i, value: i + '字符内容'.repeat(Math.random() * 20) })}this.initPositions(this.listData, this.preItemSize)}}, 

有些同学可能会想,懒加载时初始数据量较少,会导致滚动条很短,间接给用户一种数据量很少的错觉。

对于这种情况我们需要跟后端做好协调,接口返回的数据格式大致规定为这样

data: {page: 1,size: 1000,count: 10000,list: [1...1000],updateTime: '...',. . . . . .
} 

然后使用data.count初始化positions,在后续懒加载到对应索引的数据时,替换positions里的内容。

最后

为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/347762.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Centos安装OpenResty

文章目录一. OpenResty是什么二. OpenResty的安装1. 安装开发库2. 安装OpenResty仓库3. 安装OpenResty4. 安装opm工具5. 目录结构6. 配置nginx的环境变量7. 启动和运行8. 配置文件修改三. 小案例1. 案例说明2. OpenResty监听请求3. 编写业务代码4. 获取请求参数一. OpenResty是…

深度解析React性能优化API

性能优化一直是前端领域讨论的一个热门问题&#xff0c;但在平时沟通及code review过程中发现很多人对于React中性能优化理解很模糊&#xff0c;讲不清楚组件什么时候更新&#xff0c;为什么会更新&#xff0c;关于React性能优化的文章虽然比较多&#xff0c;但大多数都是在罗列…

【C/C++】内存管理详解

目录内存布局思维导图1.C/C内存分布数据段&#xff1a;栈&#xff1a;代码段&#xff1a;堆:2.C语言中动态内存管理方式3.C内存管理方式3.1new/delete操作内置类型3.2new和delete操作自定义类型4.operator new 与 operator delete函数5.new和delete的实现原理5.1内置类型5.2自定…

ChatGPT is not all you need,一文看尽SOTA生成式AI模型:6大公司9大类别21个模型全回顾(二)

文章目录ChatGPT is not all you need&#xff0c;一文看尽SOTA生成式AI模型&#xff1a;6大公司9大类别21个模型全回顾&#xff08;二&#xff09;Image-to-Text 模型FlamingoVisualGPTText-to-Video 模型PhenakiSoundifyText-to-Audio 模型AudioLMJukeboxWhisperChatGPT is n…

protoc-gen-go的使用和问题

最近 在网上查看关于proto文件编译为golang代码的文章&#xff0c;发现遇到的问题好多都是文件目录不对&#xff0c;参数不对的情况&#xff0c;这里主要解决&#xff0c;使用 不同版本的proto-gen-go 参数不一样和找不到文件问题 安装protoc-gen-go google.golang.org仓库版本…

互联网新时代要来了(二)什么是AIGC?

什么是AIGC&#xff1f; 最近&#xff0c;又火了一个词“**AIGC”**2022年被称为是AIGC元年。那么我们敬请期待&#xff0c;AIGC为我们迎接人工智能的下一个时代。 TIPS:内容来自百度百科、知乎、腾讯、《AIGC白皮书》等网页 什么是AIGC&#xff1f;1.什么是AIGC&#xff1f;…

Vue3篇.01-简介及基本使用,项目创建方式, 模板语法, 事件监听, 修饰符

一.简介1.概念Vue 是一款用于构建用户界面的 JS框架&#xff0c; 基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c; 高效地开发用户界面。渐进式框架&#xff0c; 适应不同需求进行开发。两个核心功能&#xff1a;声明式…

在云原生的趋势下,不掌握Go语言可能不太行

云原生技术已经是不可逆的趋势 云原生技术使组织能够在公共云、私有云和混合云等现代动态环境中构建和运行可扩展的应用程序,其中容器、服务网格、微服务、不可变基础设施和声明式 API 等都是云原生的重要技术内容。 这些新技术的出现使松散耦合的系统具有弹性、可管理和可观…

FPGA基于VDMA实现任意分辨率视频输出显示,高度贴近真实项目,提供工程源码和技术支持

目录1、前言2、任意分辨率视频输出理论基础3、VDMA实现数据缓存4、工程1&#xff1a;Kintex7使用VDMA5、工程2&#xff1a;Zynq7100使用VDMA6、上板调试验证并演示7、福利&#xff1a;工程代码的获取1、前言 之前写过一篇FPGA纯verilog实现任意分辨率视频输出显示&#xff0c;…

hume项目k8s的改造

hume项目k8s的改造 一、修改构建目录结构 1、在根目录下添加build-work文件夹 目录结构如下 [rootk8s-worker-01 build-work]# tree . . ├── Dockerfile ├── hume │ └── start.sh └── Jenkinsfile2、每个文件内容如下 Dockerfile FROM ccr.ccs.tencentyun…

数据结构与算法基础(王卓)(10):案例分析与实现(多项式;稀疏多项式;图书管理系统(略);)

题干&#xff1a; 用线性表和链表的方式&#xff0c;分别实现&#xff08;稀疏&#xff09;多项式的 定义&#xff08;构造框架&#xff09;创建加减乘&#xff08;多项式&#xff09;线性表的创建&#xff1a; &#xff08;略&#xff0c;相较于其他操作难度不&#xff0c;以…

亚马逊真人测评好还是自养号测评好 深度剖析讲解

关于真人测评和自养号哪个好&#xff0c;一直都是老生常谈的问题了。实际操作下来到底哪一个更好呢&#xff1f;今天陈哥给大家详细分析一下。 先说自养号。所谓的自养号&#xff0c;说白了就是通过搭建国外的真实买家环境&#xff0c;然后购买资料自己注册的账号。 很多工作…

Java300集,学完即可就业

学习java首先我们要明白Java是什么&#xff1f;Java是一门面向对象的编程语言&#xff0c;不仅吸收了C语言的各种优点&#xff0c;还摒弃了C里难以理解的多继承、指针等概念&#xff0c;因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表&a…

K8s 架构简介(一)

一、前言 在开始学习K8s之前&#xff0c;让我们对容器有一个基本的了解 1.1 什么是容器 一个容器镜像是一个可运行的软件包&#xff0c;其中包含了一个完整的可执行程序&#xff0c;包括代码和运行时需要应用、系统库和全部重要设置的默认值。 通过将应用程序本身&#xff…

虹科新闻|虹科与Telco Systems正式建立合作伙伴关系

近日&#xff0c;虹科与美国Telco Systems达成战略合作&#xff0c;虹科正式成为Telco Systems在中国区域的认证授权代理商。未来&#xff0c;虹科将携手Telco Systems&#xff0c;共同为新一代边缘计算和企业网络提供创新通信软件产品。虹科及Telco Systems双方都对彼此的合作…

C语言(逻辑运算符和条件运算符)

目录 一.逻辑运算符 1.原理 2.等级排序 3.求值顺序 二.条件运算符:? 一.逻辑运算符 1.原理 && 与&#xff08;条件都为真&#xff0c;才为真&#xff09; || 或&#xff08;一个条件为真&#xff0c;才为真&#xff09; &#xff01; 非&#xff08;条件为假&…

【R语言(二):Nomogram(诺莫图/列线图)绘制 / R语言逻辑回归分析】

R语言(二)&#xff1a;Nomogram(诺莫图/列线图)绘制 1、基本概念 Nomogram&#xff0c;中文常称为诺莫图或者列线图。简单的说是将Logistic回归或Cox回归的结果进行可视化呈现。它根据所有自变量回归系数的大小来制定评分标准&#xff0c;给每个自变量的每个取值水平一个评分&…

怎样的目标管理能真正实现目标?做到这3点就对了

目标应该是每个人人生中接触最多的一样东西了&#xff0c;大到分分钟几百万上下的项目目标&#xff0c;小到一次考试。目标能不能完成&#xff0c;关键还是看目标管理有没有做好&#xff0c;做到下面这三点&#xff0c;不论是在职场中管团队&#xff0c;还是在生活中管个人&…

十四、平衡二叉树

1、看一个案例&#xff08;说明二叉排序树可能的问题&#xff09; 给你一个数列{1,2,3,4,5,6}&#xff0c;要求创建一棵二叉排序树&#xff08;BST&#xff09;&#xff0c;并分析问题所在。 上面二叉排序树存在问题分析&#xff1a; 左子树全部为空&#xff0c;从形式上看&…

tr命令笔记

tr 是Unix命令行专家工具箱中的一件万能工具。它可用于编写优雅的单行命令。 tr 可以对 来自标准输入的内容进行字符替换、字符删除以及重复字符压缩。 tr 是translate&#xff08;转换&#xff09;的简写&#xff0c; 因为它可以将一组字符转换成另一组字符。 tr 只能…