虚拟滚动 - 从基本实现到 Angular CDK
简介
在大数据列表的处理上,虚拟滚动是一种优化性能的有效方式。本篇文章将详细介绍两种常见的虚拟滚动实现方式:使用 transform 属性和 Intersection Observer。重点讲解如何通过 transform 属性实现高效的虚拟滚动,并对比Angular CDK中的实现,探讨其在渲染优化中的应用。
虚拟滚动的基本实现(以纵向滚动为例)
常见的实现方式包括使用 transform 和 Intersection Observer。
下面我们将分别介绍这两种方法,并重点讲解 transform 的实现方式。
transform
使用 transform 实现虚拟滚动,通过计算总高度、动态渲染可见数据项,并使用 transform 调整位置。
+------------------+ <--- scroll-container (viewport)
| |
| +--------------+ |
| | Item 101 | | <--- transform: translateY(5000px)
| +--------------+ |
| | Item 102 | | <--- transform: translateY(5050px)
| +--------------+ |
| | Item 103 | | <--- transform: translateY(5100px)
| +--------------+ |
| | Item 104 | | <--- transform: translateY(5150px)
| +--------------+ |
| | Item 105 | | <--- transform: translateY(5200px)
| +--------------+ |
| | ... | |
| +--------------+ |
| |
+------------------+
HTML
容器的高度根据数据项的总数和每个列表项的高度动态设置,使滚动条可以覆盖整个数据集。
每个列表项通过 transform:translateY
属性调整可见数据项的位置,以补足未渲染元素的空间。
<divclass="virtual-scroll-body"[style.height.px]="data.length * listItemHeight"
><divclass="virtual-scroll-item"*ngFor="let item of showList; let index = index; trackBy: trackBy"[style.height.px]="listItemHeight"[style.transform]="'translateY(' + translateHeight + 'px)'">{{ item.name }}</div>
</div>
CSS
确保容器可以垂直滚动,同时宽度固定。
每个列表项的样式设定有边框,并且没有垂直溢出。
:host {display: inline-block;height: 400px;width: 100%;overflow-y: auto;overflow-x: hidden;.virtual-scroll-item {overflow-y: hidden;border: 1px solid #eee;}
}
TS
scroll 事件监听器根据滚动位置更新 translateHeight 和 startIndex,重新计算可渲染的数据项。
public data: TransferList[] = this.dataService.generateData(100);
public listItemHeight: number = 50;
public translateHeight: number = 0;
public showList: TransferList[] = this.data.slice(0, 20);@HostListener("scroll", ["$event"])
scroll(event: Event) {const scrollTop = (event.target as HTMLElement).scrollTop;const startIndex = Math.floor(scrollTop / this.listItemHeight);this.translateHeight = startIndex * this.listItemHeight;this.showList = this.data.slice(startIndex, startIndex + 20);
}
Intersection Observer
Intersection Observer 是一个用于检测元素是否进入视口的 API,也可以用来实现虚拟滚动。它能够在目标元素进入或离开视口时触发回调,从而动态加载或卸载元素。
@ViewChild('startMarker', { static: true }) startMarker: ElementRef;
@ViewChild('endMarker', { static: true }) endMarker: ElementRef;private observer = new IntersectionObserver(entries => {entries.forEach(entry => {if (entry.isIntersecting) {// 处理元素进入视口的逻辑}});},{root: this.elementRef.nativeElement,threshold: 0.1}
);ngOnInit(): void {this.observer.observe(this.startMarker.nativeElement);this.observer.observe(this.endMarker.nativeElement);
}ngOnDestroy(): void {this.observer.disconnect();
}
由于 Intersection Observer 的复杂性和性能开销,以及在虚拟滚动场景下不完全必要,所以就不过多介绍了。如果有需要,可以专门介绍一下这个 API。其实它更适用于需要精确监控元素可见性的场景,例如懒加载图像、触发动画等。而虚拟滚动的核心需求是减少 DOM 渲染数量,提升性能,transform 在这种情况下已经能很好地满足需求。因此,大部分实现虚拟滚动时更倾向于选择 transform 这种传统且成熟的方法,Angular CDK 实现虚拟滚动也是同样采用 transform 来进行实现。
Angular CDK 虚拟滚动
上文提到,CDK 通过 transform 实现虚拟滚动。接下来,我们将深入探讨其具体实现方式,并将 CDK 的实现与之前的简单实现进行对比,以便更好地理解其中的原理。
CdkFixedSizeVirtualScroll
内容已知固定大小的虚拟滚动策略(正式版本中仅有这一种策略)
文件支持三个传入:
- itemSize:列表中项目的大小。
- minBufferPx:在视口之外渲染的最小缓冲区,如果缓冲区低于这个数字,将加载更多的数据项。
- maxBufferPx:加载更多数据项时要渲染的缓冲区。
文件实现以下函数:
- attach:将容器附加到滚动策略中,并完成首次更新视口的渲染范围。
- onContentScrolled:当视图端口滚动时调用,具体实现为_updateTotalContentSize 函数。 onDataLengthChanged:当数据的长度发生变化时调用。
- _updateTotalContentSize:计算容器总高度,更新视口的总内容大小。
- updateRenderedRange:更新视口的渲染范围,根据滚动位置重新计算可渲染的数据项下标
_updateTotalContentSize:计算容器总高度
简单实现
[style.height.px]="data.length * listItemHeight"
CDK
this._viewport.setTotalContentSize(this._viewport.getDataLength() * this._itemSize);
通过将所有数据项的数量乘以每个数据项的固定高度来计算容器的总高度。
_updateRenderedRange:根据滚动位置重新计算可渲染的数据项下标。
简单实现
const scrollTop = (event.target as HTMLElement).scrollTop;
const startIndex = Math.floor(scrollTop / this.listItemHeight);
在 Angular CDK 的虚拟滚动实现中,渲染数据项的计算不仅仅是简单的滚动偏移量除以数据项的固定高度。引入缓冲区的概念后,渲染范围的调整变得更加复杂。下面是这个过程的详细解释:
获取必要的值
- 当前已渲染的数据项范围:通常初始值为 {start: 0, end: 0} 。
- 视口大小:根据是横向还是纵向滚动,取视口的宽度或高度。
- 数据项的总数量。
- 当前容器的滚动偏移量。
调整渲染范围
- 起始缓冲区:指第一个渲染项与视口可见区域之间的距离。
- 如果起始缓冲区小于最小缓冲区,并且当前渲染的起始项不是第一个项,说明是向上(左)滚动就需要向上(左)扩展渲染范围
- 计算需要向上扩展的项数,确保渲染范围的起始项不小于0。
- 更新渲染范围的结束项,确保不超过数据总长度。
- 如果起始缓冲区小于最小缓冲区,并且当前渲染的起始项不是第一个项,说明是向上(左)滚动就需要向上(左)扩展渲染范围
- 结束缓冲区:指最后一个渲染项与视口可见区域之间的距离。
- 如果结束缓冲区小于最小缓冲区,并且当前渲染的结束项不是最后一个项,说明是向下(右)滚动就需要向下(右)扩展渲染范围:
- 计算需要向下扩展的项数,确保渲染范围的结束项不超过数据总长度。
- 更新渲染范围的起始项,确保不低于0。
- 如果结束缓冲区小于最小缓冲区,并且当前渲染的结束项不是最后一个项,说明是向下(右)滚动就需要向下(右)扩展渲染范围:
这一机制确保了在用户滚动时,视口内外有足够的缓冲区,能够更加的顺滑。具体的代码实现和示意图已放在附录二和附录三中,可帮助进一步理解。
CdkVirtualScrollViewport && CdkVirtualForOf
CdkVirtualForOf 负责数据源的管理和渲染逻辑。
CdkVirtualScrollViewport 是在 CdkVirtualForOf 的帮助下虚拟滚动的视图组件,实现了以下四个功能:
- 初始化时将视图附加到滚动策略中
- 声明并订阅滚动事件的可观察对象
- 管理当前在容器中可渲染的数据项
- 管理当前容器可渲染数据项偏移量
初始化时将视图附加到滚动策略中 & 声明并订阅滚动事件的可观察对象
this.ngZone.runOutsideAngular(() =>Promise.resolve().then(() => {this._measureViewportSize();this._scrollStrategy.attach(this);this.scrollable.elementScrolled().pipe(startWith(null), auditTime(0, SCROLL_SCHEDULER), takeUntil(this._destroyed)).subscribe(() => this._scrollStrategy.onContentScrolled());this._markChangeDetectionNeeded();})
管理当前在容器中可渲染的数据项
简单实现
this.showList = this.data.slice(startIndex, startIndex + 20);
CDK
this._viewport.renderedRangeStream.pipe(takeUntil(this._destroyed)).subscribe(range => {this._renderedItems = this._data.slice(this._renderedRange.start, this._renderedRange.end);
});
通过可渲染的数据项范围来更新要显示的数据,但 CDK 通过流的方式提供了更为复杂和灵活的渲染控制。
管理当前容器可渲染数据项偏移量
简单实现
this.translateHeight = startIndex * this.listItemHeight;
CDK
this._viewport.setRenderedContentOffset(this._itemSize * newRange.start);
通过计算第一个可渲染数据项的索引乘以每个数据项的高度来确定偏移量。
总结
在虚拟滚动的实现中,无论是简单实现还是 Angular CDK 的企业级解决方案,核心都围绕以下三点展开:
- 计算总高度:用于撑起滚动效果,使滚动条能够覆盖整个数据集。
- 实时更新可见数据项的范围:通过只渲染视口内的数据项,减少 DOM 节点的数量,提高性能。
- 设置偏移量:确保渲染的 DOM 元素处于正确的位置,保持用户体验的一致性。
Angular CDK 提供了更加成熟和优化的解决方案,不仅包括精细的渲染控制和灵活的配置选项,还在性能表现上有显著提升。特别是引入缓冲区的概念,使得滚动更加平滑流畅,在处理大数据量时更加得心应手。
附录
附录一:文件关系图
附录二:_updateRenderedRange源码
private _updateRenderedRange() {if (!this._viewport) {return;}//获取当前渲染数据范围 默认是{start:0,end:0}const renderedRange = this._viewport.getRenderedRange();const newRange = {start: renderedRange.start, end: renderedRange.end};//获取当前视口大小 区分横向和纵向滚动 clientHeight || clientWidthconst viewportSize = this._viewport.getViewportSize();//获取总数据容量const dataLength = this._viewport.getDataLength();//当前视图滚动偏移量let scrollOffset = this._viewport.measureScrollOffset();//计算当前视图可见的索引 不一定是整数let firstVisibleIndex = this._itemSize > 0 ? scrollOffset / this._itemSize : 0;//....// 计算起始缓冲区const startBuffer = scrollOffset - newRange.start * this._itemSize;//startBuffer < this._minBufferPx:如果起始缓冲区小于最小缓冲区(_minBufferPx),意味着需要扩展渲染范围。//newRange.start != 0:如果当前渲染的起始索引不是0,才能进行扩展。if (startBuffer < this._minBufferPx && newRange.start != 0) {//计算需要扩展的项数const expandStart = Math.ceil((this._maxBufferPx - startBuffer) / this._itemSize);//newRange.start 更新渲染起始项,确保不小于0。//newRange.end 更新渲染结束项,确保不超过数据总长度。newRange.start = Math.max(0, newRange.start - expandStart);newRange.end = Math.min(dataLength,Math.ceil(firstVisibleIndex + (viewportSize + this._minBufferPx) / this._itemSize),);} else {//计算结束缓冲区const endBuffer = newRange.end * this._itemSize - (scrollOffset + viewportSize);//startBuffer < this._minBufferPx:如果起始缓冲区小于最小缓冲区(_minBufferPx),意味着需要扩展渲染范围。//newRange.end != dataLength:如果当前渲染的结束索引不是数据总长度,才能进行扩展。if (endBuffer < this._minBufferPx && newRange.end != dataLength) {//计算需要扩展的项数,公式为 Math.ceil((this._maxBufferPx - startBuffer) / this._itemSize)。const expandEnd = Math.ceil((this._maxBufferPx - endBuffer) / this._itemSize);if (expandEnd > 0) {newRange.end = Math.min(dataLength, newRange.end + expandEnd);newRange.start = Math.max(0,Math.floor(firstVisibleIndex - this._minBufferPx / this._itemSize),);}}}this._viewport.setRenderedRange(newRange);this._viewport.setRenderedContentOffset(this._itemSize * newRange.start);this._scrolledIndexChange.next(Math.floor(firstVisibleIndex));}
附录三:CDK虚拟滚动示意图
相关文章:

虚拟滚动 - 从基本实现到 Angular CDK
简介 在大数据列表的处理上,虚拟滚动是一种优化性能的有效方式。本篇文章将详细介绍两种常见的虚拟滚动实现方式:使用 transform 属性和 Intersection Observer。重点讲解如何通过 transform 属性实现高效的虚拟滚动,并对比Angular CDK中的实…...
Spring WebFlux学习笔记(一)
核心思想 WebFlux主要是异步 例子 参考一个源码: https://blog.csdn.net/qq_43923045/article/details/106309432?spm1001.2014.3001.5506 GetMapping("/delay1")public Mono<RestResult> delayResult() {long l System.currentTimeMillis();…...
富格林:正确追损思维安全交易
富格林指出,对于如何正确追损的这个问题是需要持续付出时间和精力的,发现具备耐心的投资者往往在正确追损的路上更加游刃有余。他们总是可以保持较为平和的心态,不急不躁地分析原因并通过自身掌握的安全应对措施来进行交易。富格林在以下分享…...
前端vue2迁移至uni-app
1.确定文件存放位置 components: 继续沿用 pages: views内容移动到pages static: assets内容移动到static uni_modules: uni-app的插件存放位置 迁移前 src├─assets│ └─less├─components│ ├─common│ │ ├─CommentPart│ │ └─MessDetail│ ├─home│…...

恋爱脑学Rust之闭包三Traits:Fn,FnOnce,FnMut
在Rust中,FnOnce、FnMut和Fn是三个用于表示闭包(closure)类型的trait。闭包是一种特殊的函数,它可以捕获其环境变量,即在其定义时所处的作用域中的变量。以下是关于这三个trait的详细介绍: 1. FnOnce&#…...
区块链介绍
区块链(英文名:blockchain或block chain)是一种块链式存储、不可篡改、安全可信的去中心化分布式账本,它结合了分布式存储、点对点传输、共识机制、密码学等技术,通过不断增长的数据块链(Blocks)…...
git回滚间隔的提交
如果你需要回滚几个非连续的提交,可以使用 git revert 来选择性地撤销这些提交。这样做不会改变提交历史,只是会在当前分支上创建新的提交来反转指定的更改。 ### 使用 git revert 回滚间隔的提交 1. **查看提交历史**: 首先,…...

Map和Set(数据结构)
一、概念 Map 和 set 是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。 Map 和 Set 是一种适合动态查找的集合容器。 模型 一般把搜索的数据称为关键字( Key ),和关键字对应的称为值࿰…...

vue3uniapp实现自定义拱形底部导航栏,解决首次闪烁问题
前言: 我最初在网上翻阅查找了很多方法,发现大家都是说在page.json中tabbar中添加:"custom": true,即可解决首次闪烁的问题,可是添加了我这边还是会闪烁,因此我这边改变了思路,使用了虚拟页面来解…...
新需求编码如何注意低级错误代码
1. 日常开发常见错误问题 变量拷贝未修改变量定义的值刚开始是随意写的一个值,想等到上线的时候再改成正确的,但是上线的时候忘记改了程序常量配置的错误逻辑关系判断错误 常见的如都不为null、都不为空集合判断不为空逻辑取反了多个关系的 && …...

系统架构图设计(行业领域架构)
物联网 感知层:主要功能是感知和收集信息。感知层通过各种传感器、RFID标签等设备来识别物体、采集信息,并对这些信息进行初步处理。这一层的作用是实现对物理世界的感知和初步处理,为上层提供数据基础网络层:网络层负责处理和传输…...
windows 文件监控 c++ 11及以上版本可用
在该版本上稍微改了一下https://blog.csdn.net/weixin_50964512/article/details/125002563 #include<iostream> #include<string> #include<Windows.h> #include<list> #include<locale> using namespace std;class WatchFolder {HANDLE m_hFi…...

jsMind:炸裂项目,用JavaScript构建的思维导图库,GitHub上的热门开源项目
嗨,大家好,我是小华同学,关注我们获得“最新、最全、最优质”开源项目和工作学习方法 jsMind 是一个基于 JavaScript 的思维导图库,它利用 HTML5 Canvas 和 SVG 技术构建,可以轻松地在网页中嵌入和编辑思维导图。它以 …...

postman的脚本设置接口关联
pm常用的对象 变量基础知识 postman获取响应结果的脚本的编写 下面是购物场景存在接口信息的关联 登录进入---搜索商品---进入商品详情---加入购物车 资源在附件中,可以私聊单独发送 postman的SHA256加密 var CryptoJS require(crypto-js);// 需要加密的字符串 …...

【python】OpenCV—Tracking(10.3)—GOTURN
文章目录 1、功能描述2、模型介绍3、代码实现4、完整代码5、结果展示6、优缺点分析7、参考 1、功能描述 基于 Generic Object Tracking using Regression Networks 方法,实现单目标跟踪 2、模型介绍 (1)发表来自 Held D, Thrun S, Savarese…...
git pull遇到一个问题
shell request failed on channel 0 需要修改服务器配置[rootadmin ~]# cat /etc/security/limits.d/20-nproc.conf # Default limit for number of users processes to prevent # accidental fork bombs. # See rhbz #432903 for reasoning.* soft nproc 409…...

书生-第四期闯关:完成SSH连接与端口映射并运行hello_world.py
端口映射完成后,访问127.0.0.1:7860成功展示如下界面: 书生浦语大模型实战营 项目地址:https://github.com/InternLM/Tutorial/...

【CSS3】css开篇基础(5)
1.❤️❤️前言~🥳🎉🎉🎉 Hello, Hello~ 亲爱的朋友们👋👋,这里是E绵绵呀✍️✍️。 如果你喜欢这篇文章,请别吝啬你的点赞❤️❤️和收藏📖📖。如果你对我的…...

AI产品独立开发变现实战营,炒掉老板做自由职业赚大钱
课程背景 在经济下行和外部就业压力增大的背景下,为解决程序员的焦虑、失业和被裁员,我们开始了这门课程,课程基于3个真实已经盈利的商业项目,从0到1带你实践AI产品的设计、开发、运营和盈利模式的全流程开发。 课程特色 增加‘…...

【UE5.3 Cesium for Unreal】编译GlobePawn
目录 前言 效果 步骤 一、下载所需文件 二、下载CesiumForUnreal插件 三、处理下载的文件 四、修改代码 “CesiumForUnreal.uplugin”部分 “CesiumEditor.cpp”部分 “CesiumEditor.h”部分 “CesiumPanel.cpp”部分 “IonQuickAddPanel.cpp”部分 “IonQuickAd…...

Zustand 状态管理库:极简而强大的解决方案
Zustand 是一个轻量级、快速和可扩展的状态管理库,特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...

安宝特方案丨XRSOP人员作业标准化管理平台:AR智慧点检验收套件
在选煤厂、化工厂、钢铁厂等过程生产型企业,其生产设备的运行效率和非计划停机对工业制造效益有较大影响。 随着企业自动化和智能化建设的推进,需提前预防假检、错检、漏检,推动智慧生产运维系统数据的流动和现场赋能应用。同时,…...

基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...
TRS收益互换:跨境资本流动的金融创新工具与系统化解决方案
一、TRS收益互换的本质与业务逻辑 (一)概念解析 TRS(Total Return Swap)收益互换是一种金融衍生工具,指交易双方约定在未来一定期限内,基于特定资产或指数的表现进行现金流交换的协议。其核心特征包括&am…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...

ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...
Webpack性能优化:构建速度与体积优化策略
一、构建速度优化 1、升级Webpack和Node.js 优化效果:Webpack 4比Webpack 3构建时间降低60%-98%。原因: V8引擎优化(for of替代forEach、Map/Set替代Object)。默认使用更快的md4哈希算法。AST直接从Loa…...

解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...