虚拟滚动 - 从基本实现到 Angular CDK

news2024/11/6 21:35:29

简介

在大数据列表的处理上,虚拟滚动是一种优化性能的有效方式。本篇文章将详细介绍两种常见的虚拟滚动实现方式:使用 transform 属性和 Intersection Observer。重点讲解如何通过 transform 属性实现高效的虚拟滚动,并对比Angular CDK中的实现,探讨其在渲染优化中的应用。

虚拟滚动的基本实现(以纵向滚动为例)

常见的实现方式包括使用 transformIntersection 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 属性调整可见数据项的位置,以补足未渲染元素的空间。

<div
  class="virtual-scroll-body"
  [style.height.px]="data.length * listItemHeight"
>
  <div
    class="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 的企业级解决方案,核心都围绕以下三点展开:

  1. 计算总高度:用于撑起滚动效果,使滚动条能够覆盖整个数据集。
  2. 实时更新可见数据项的范围:通过只渲染视口内的数据项,减少 DOM 节点的数量,提高性能。
  3. 设置偏移量:确保渲染的 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 || clientWidth
    const 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虚拟滚动示意图
在这里插入图片描述

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

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

相关文章

Spring Boot 配置文件启动加载顺序

前言 Spring Boot的启动加载顺序是一个涉及多个步骤和组件的过程。Spring Boot通过一系列默认设置简化了应用程序的配置&#xff0c;使得开发者能够快速地搭建和部署应用。为了实现这一目标&#xff0c;Spring Boot采用了一种分层和优先级机制来加载配置文件。 一、Spring Bo…

C# Modbus RTU通讯回顾

涉及技术&#xff1a; 1.使用NMdbus4 库 2.ushort[]转int 记得之前刚学习的时候&#xff0c;是ushort[] → Hex字符串→byte[] → 翻转byte[] →BitConverter.ToInt32()&#xff0c;饶了一大圈&#xff1b;实际上可以直接转&#xff1b;这里也有小细节&#xff1a;使用BitCo…

HFSS学习笔记(五)金属过孔、复制模型带激励等问题(持续更新...)

HFSS学习笔记&#xff08;五&#xff09;金属过孔、复制模型带激励等问题&#xff08;持续更新…&#xff09; 一、金属过孔设计 方法一&#xff1a;用介质减去金属圆柱体&#xff0c;然后再添加金属圆柱体 方法二&#xff1a;嵌入金属圆柱 圆柱过孔选择材料为“copper” HFS…

Late Chunking×Milvus:如何提高RAG准确率

01. 背景 在RAG应用开发中&#xff0c;第一步就是对于文档进行chunking&#xff08;分块&#xff09;&#xff0c;高效的文档分块&#xff0c;可以有效的提高后续的召回内容的准确性。而对于如何高效的分块是个讨论的热点&#xff0c;有诸如固定大小分块&#xff0c;随机大小分…

大屏可视化:舞动数据与美观的“设计秘籍”

大屏可视化鉴赏&#xff1a;踏入软件系统产品设计之旅&#xff0c;让我们一同鉴赏那些闪耀在智慧农业、智慧园区、智慧社区及智慧港口等领域的大屏可视化杰作。每一帧画面&#xff0c;都是科技与创新的完美融合&#xff0c;数据跃然屏上&#xff0c;智慧触手可及。 >> 数…

基于STM32的智能声音跟随小车设计

引言 本项目基于STM32微控制器设计了一个智能声音跟随小车&#xff0c;通过集成麦克风阵列实现声音源定位和跟随功能。该系统可以检测环境中的声音信号&#xff0c;如手掌拍击声或语音指令&#xff0c;驱动小车向声源方向移动。项目涉及硬件设计、声音信号处理算法以及电机控制…

Bruno解决SSL验证问题

在测试接口的时候&#xff0c;我使用的是Bruno这个软件&#xff0c;开源离线的API测试软件。 主页是这样子的 今天在测试一个HTTPS的接口时候&#xff0c;因为这个HTTPS接口是用的是自签证书&#xff0c;所以就报错误了。 Error invoking remote method send-http-request: …

【论文速读】| APOLLO:一种基于 GPT 的用于检测钓鱼邮件并生成警告用户的解释的工具

基本信息 原文标题&#xff1a;APOLLO: A GPT-based tool to detect phishing emails and generate explanations that warn users 原文作者&#xff1a;Giuseppe Desolda, Francesco Greco, Luca Vigan 作者单位&#xff1a;University of Bari “A. Moro”, Italy, King’…

jfrog artifactory oss社区版,不支持php composer私库

一、docker安装 安装环境&#xff1a;centos操作系统&#xff0c;root用户。 如果是mac或ubuntu等操作系统的话&#xff0c;会有许多安装的坑等着你。 一切都是徒劳&#xff0c;安装折腾那么久&#xff0c;最后还是不能使用。这就是写本文的初衷&#xff0c;切勿入坑就对了。 …

WindowsDocker安装到D盘,C盘太占用空间了。

Windows安装 Docker Desktop的时候,默认位置是安装在C盘,使用Docker下载的镜像文件也是保存在C盘,如果对Docker使用评率比较高的小伙伴,可能C盘空间,会被耗尽,有没有一种办法可以将Docker安装到其它磁盘,同时Docker的数据文件也保存在其他磁盘呢? 答案是有的,我们可以…

vue常见题型(1-10)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 2.2双向绑定的原理是什么vue框架采用的是数据双向绑定的方式&#xff0c;由三个重要部分构成2.2.1.ViewModel2.2.2 双向绑定2.2.3.1.编译Compile2.2.3.2.依赖收集 3…

python怎么将字符串转换为数字

python如何将列表中的字符串转为数字&#xff1f;具体方法如下&#xff1a; 有一个数字字符的列表&#xff1a; numbers [1, 5, 10, 8] 想要把每个元素转换为数字&#xff1a; numbers [1, 5, 10, 8] 用一个循环来解决&#xff1a; new_numbers []; for n in numbers:new_n…

大数据新视界 -- 大数据大厂之 Impala 性能优化:解锁大数据分析的速度密码(上)(1/30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Flutter 插件 sliding_up_panel 实现从底部滑出的面板

前言 sliding_up_panel 是一个 Flutter 插件&#xff0c;用于实现从底部滑出的面板。它在设计上非常灵活&#xff0c;能够适应多种 UI 场景&#xff0c;比如从底部滑出的菜单、可拖动的弹出面板等。以下是 sliding_up_panel 的详细用法&#xff0c;包括常用的参数说明和示例代…

大客户营销数字销售实战讲师培训讲师唐兴通专家人工智能大模型销售客户开发AI大数据挑战式销售顾问式销售专业销售向高层销售业绩增长创新

唐兴通 销售增长策略专家、数字销售实战导师 专注帮助企业构建面向AI数字时代新销售体系&#xff0c;擅长运用数字化工具重塑销售流程&#xff0c;提升销售业绩。作为《挑战式销售》译者&#xff0c;将全球顶尖销售理论大师马修狄克逊等理论导入中国销售业界。 核心专长&…

es数据同步(仅供自己参考)

数据同步的问题分析&#xff1a; 当MySQL进行增删改查的时候&#xff0c;数据库的数据有所改变&#xff0c;这个时候需要修改es中的索引库的值&#xff0c;这个时候就涉及到了数据同步的问题 解决方法&#xff1a; 1、同步方法&#xff1a; 当服务对MySQL进行增删改的时候&…

入门车载以太网(3) -- 网络层

目录 1. 网络通信示例 2. IP地址类别 3. IP数据报 4. 小结 今天继续车载以太网&#xff0c;聊聊网络层。 1. 网络通信示例 我们首先回顾车载以太网的数据传输模型。 从7层开始(车载以太网模糊了5-7层&#xff0c;统称应用层)&#xff0c;每个中间层都为上层提供功能&…

六个核桃斥资千万研究脑健康,核桃健脑作用科学具象化了

健康&#xff0c;是这两年热度居高不下的社会话题。对健康的追求影响了诸多领域的发展&#xff0c;上至尖端科研&#xff0c;下至日常接触的食品饮料&#xff0c;都已被卷入大势。 其中&#xff0c;“脑健康”这个听起来更前沿的话题&#xff0c;又已经成为格外重要的一个领域…

基于Multisim光控夜灯LED电路(含仿真和报告)

【全套资料.zip】光控夜灯LED电路设计Multisim仿真设计数字电子技术 文章目录 功能一、Multisim仿真源文件二、原理文档报告资料下载【Multisim仿真报告讲解视频.zip】 功能 1.采用纯数字电路&#xff0c;非单片机。 2.通过检测周围光线&#xff0c;光线暗且有声音时自动开灯…

【go从零单排】go中的基本数据类型和变量

Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 基本类型 go中的string、int、folat都可以用连接boolen可以用逻辑表达式计算 package mainimport "fmt&quo…