
Angular CDK Scrolling 包源码与 API 全解ScrollDispatcher、CdkScrollable 与高性能虚拟滚动实战【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components导读angular/cdk/scrolling是 Angular CDK 中负责滚动基础设施与高性能列表渲染的核心包它一方面通过CdkScrollable指令、ScrollDispatcher与ViewportRuler服务让组件能够统一监听任意祖先滚动容器的事件并度量浏览器视口另一方面通过cdk-virtual-scroll-viewport与*cdkVirtualFor提供虚拟滚动能力仅渲染视口内可见的条目从而流畅承载数万乃至十万级数据列表。本文以仓库内的 API 报告 为骨架、以 官方使用文档 为实操主线并结合 src/cdk/scrolling 下的完整源码实现逐层拆解该包的公开类型、指令、服务与注入令牌给出可复制可运行的配置示例与缓冲策略计算示例。一、包结构一览从 public-api 到 ScrollingModulescrolling包的全部公开符号集中在 public-api.ts并按职责组织为下列源文件源文件核心公开符号职责scrollable.tsCdkScrollable、ExtendedScrollToOptions及相关坐标类型将元素标记为可滚动容器并注册到 dispatcherscroll-dispatcher.tsScrollDispatcher、ScrollDispatcherTarget、DEFAULT_SCROLL_TIME汇总全应用滚动事件、查询祖先滚动容器viewport-ruler.tsViewportRuler、ViewportScrollPosition、DEFAULT_RESIZE_TIME度量浏览器视口尺寸与滚动位置virtual-scroll-viewport.tsCdkVirtualScrollViewport、CDK_VIRTUAL_SCROLL_VIEWPORT虚拟滚动视口组件virtual-for-of.tsCdkVirtualForOf、CdkVirtualForOfContext视口内的*ngFor替代指令fixed-size-virtual-scroll.tsCdkFixedSizeVirtualScroll、FixedSizeVirtualScrollStrategy固定尺寸条目的滚动策略virtual-scroll-strategy.tsVirtualScrollStrategy、VIRTUAL_SCROLL_STRATEGY可扩展的滚动策略接口与令牌virtual-scrollable.tsCdkVirtualScrollable、VIRTUAL_SCROLLABLE虚拟滚动滚动容器的抽象基类virtual-scrollable-element.tsCdkVirtualScrollableElement以祖先元素作为滚动容器virtual-scrollable-window.tsCdkVirtualScrollableWindow以 window 作为滚动容器scrolling-module.tsScrollingModule模块聚合入口API 报告显示ScrollingModule同时导出BidiModule、CdkScrollableModule、CdkFixedSizeVirtualScroll、CdkVirtualForOf、CdkVirtualScrollViewport、CdkVirtualScrollableWindow与CdkVirtualScrollableElement。因此在模块化NgModule应用中只需引入ScrollingModule即可在模板中直接使用cdk-virtual-scroll-viewport、*cdkVirtualFor、cdkScrollable等指令。二、滚动基础设施一CdkScrollable 指令与 ScrollDispatcher 服务2.1 CdkScrollable把元素登记为滚动容器CdkScrollable是一个选择器为[cdk-scrollable], [cdkScrollable]的指令见 scrollable.ts应被应用到任何充当滚动容器的元素上。它实现了ScrollDispatcherTarget接口export interface ScrollDispatcherTarget { elementScrolled(): ObservableEvent; getElementRef(): ElementRefHTMLElement; }在其ngOnInit生命周期中scrollable.ts指令会通过ngZone.runOutsideAngularRenderer2.listen为该元素挂载scroll事件监听把事件推入_elementScrolled的Subject避免滚动事件触发 Angular 变更检测调用scrollDispatcher.register(this)将自身登记到ScrollDispatcher的scrollContainersMap 中。销毁时ngOnDestroy则会解绑监听、complete 流并deregister。2.2 ScrollDispatcher统一滚动事件中枢ScrollDispatcherscroll-dispatcher.ts维护着一张「可滚动容器 → 订阅」的scrollContainers: MapScrollDispatcherTarget, Subscription并对外提供三个核心 APIscrolled(auditTimeInMs DEFAULT_SCROLL_TIME)返回一个 Observable任何已注册容器或 window/document/body发生滚动时都会发射事件。默认以DEFAULT_SCROLL_TIME 20毫秒节流scroll-dispatcher.ts。若传入0则直接使用未节流的 Subject避免auditTime(0)带来的额外处理开销。在 SSR非浏览器环境下直接返回observableOf()。ancestorScrolled(elementOrElementRef, auditTimeInMs?)只发射指定元素的祖先滚动容器的滚动事件实现上通过getAncestorScrollContainers逐级向上遍历parentElement判断元素是否被容器包含。register / deregister注册与注销滚动容器。值得注意的是ScrollDispatcher.scrolled()建立时会在document上挂一个全局scroll监听_renderer.listen(document, scroll, ...)并在最后一个订阅者取消订阅时自动卸载引用计数_scrolledCount。官方文档强调所有事件都在 Angular zone 之外发射若需要在回调里更新数据绑定应显式使用NgZone.run包裹。典型用法在某个组件里监听其祖先滚动容器import {ScrollDispatcher} from angular/cdk/scrolling; constructor(private scrollDispatcher: ScrollDispatcher) {} ngOnInit() { this.scrollDispatcher.ancestorScrolled(this.host, 50).subscribe(() { // 滚动回调事件已带 50ms 节流 }); }2.3 统一滚动 APIscrollTo 与 measureScrollOffset 的 RTL 归一化CdkScrollable提供了两个比原生 API 更稳健的方法核心价值是在 RTL 环境下归一化浏览器的差异scrollTo(options: ExtendedScrollToOptions)ExtendedScrollToOptions _XAxis _YAxis ScrollOptions其中_XAxis是left/right/start/end的互斥联合_YAxis是top/bottom的互斥联合通过_XOR类型实现。源码会将start/end按 RTL 语义重写为left/right把bottom重写为scrollHeight - clientHeight - bottom并针对浏览器三种 RTL 滚动轴类型NORMAL/INVERTED/NEGATED来自angular/cdk/platform的getRtlScrollAxisType分别换算最后若浏览器支持scroll-behavior则调用el.scrollTo(options)否则回退到手动设置scrollTop/scrollLeft见 scrollable.ts。measureScrollOffset(from: top | left | right | bottom | start | end)度量到某一边缘的滚动偏移同样对 RTL 做了归一化——left/right始终指容器物理左右侧start/end在 LTR 下对应左右、在 RTL 下相反。三、滚动基础设施二ViewportRuler 视口度量服务ViewportRulerviewport-ruler.ts是可注入的视口度量服务主要 API 如下API说明getViewportSize(): Readonly{width, height}返回浏览器视口宽高window.innerWidth/innerHeight带缓存getViewportRect()返回视口 DOMRect 语义的{top, left, bottom, right, height, width}getViewportScrollPosition(): ViewportScrollPosition返回{top, left}滚动位置change(throttleTime DEFAULT_RESIZE_TIME): ObservableEvent视口尺寸变化流默认以DEFAULT_RESIZE_TIME 20ms 节流源码细节值得注意构造时在 zone 外为window的resize与orientationchange事件注册监听事件进入_changeSubjectgetViewportRect使用document.documentElement.getBoundingClientRect()而非window.scrollY/scrollX注释中解释了原因Chrome/IE 在 pinch-zoom 场景下窗口滚动属性与 client 坐标可能处于不同概念视口且 body 默认有 8px margin用documentElement更一致viewport-ruler.tsgetViewportScrollPosition依次回退-documentRect.top、document.body?.scrollTop、window.scrollY、documentElement.scrollTop以兼容 Chrome 与 Firefox 对「滚动元素是 body 还是 documentElement」的分歧。应用场景配合虚拟滚动CdkVirtualScrollViewport构造时就订阅了viewportRuler.change()一旦窗口尺寸变化便调用checkViewportSize()重新度量视口并触发策略重算见 virtual-scroll-viewport.ts。四、虚拟滚动核心cdk-virtual-scroll-viewport 的工作原理4.1 为什么需要虚拟滚动官方文档明确指出在任意浏览器中加载成百上千个 DOM 元素都会变慢。虚拟滚动与无限滚动infinite scroll不同——无限滚动先渲染一批滚到底部再渲染下一批而虚拟滚动从一开始就让容器高度等于全部元素的总高度但只渲染视口内可见的那部分条目从而模拟出所有条目都已渲染的效果。在CdkVirtualScrollViewport组件内部分为两层结构见 virtual-scroll-viewport.htmldiv #contentWrapper classcdk-virtual-scroll-content-wrapper ng-content/ng-content /div div classcdk-virtual-scroll-spacer [style.width]_totalContentWidth() [style.height]_totalContentHeight()/divspacer尺寸由策略计算出的_totalContentSize决定撑出与全部条目总高/总宽一致的滚动空间使滚动条按完整数据集的比例呈现content-wrapper包裹实际渲染的子集通过style.transform translateY/translateX(offset px)将渲染子集平移到正确位置见 virtual-scroll-viewport.ts。4.2 视图生命周期与变更检测在浏览器环境中视口于ngOnInit的微任务阶段Promise.resolve().then(...)zone 外执行完成首次度量并attach滚动策略随后用auditTime(0, animationFrameScheduler)合并同一帧内的多次滚动事件再调用strategy.onContentScrolled()virtual-scroll-viewport.ts。SSR 下!this._platform.isBrowser直接跳过度量逻辑。变更检测方面视口通过effectApplicationRef注入器在变更检测前触发_doChangeDetection内部调用markForCheck()从根部检测因为条目是内容投影进来的并在渲染后通过afterNextRender回调策略的onContentRendered。整套机制把滚动与渲染过程都控制在 Angular zone 之外减少不必要的变更检测循环。4.3 关键输入与输出成员类型默认值说明orientationhorizontal \| verticalvertical滚动方向切换时重算 spacer 尺寸appendOnlybooleanfalse滚出视口后是否保留已渲染视图scrolledIndexChangeObservablenumber—视口中首个可见元素索引变化流惰性订阅策略事件避免无监听者时白白计算renderedRangeStreamObservableListRange—当前渲染索引区间{start, end}变化流renderedContentOffsetObservablenumber—渲染内容起始处距视口起点的像素偏移注意scrolledIndexChange的实现刻意不使用EventEmitter而是自定义Observable在订阅时才subscribe到策略的scrolledIndexChange并在发射前用Promise.resolve().then(() this.ngZone.run(...))把事件带回 zonevirtual-scroll-viewport.ts。五、*cdkVirtualFor视口内的列表渲染指令5.1 与 *ngFor 同 API 的基础用法*cdkVirtualFor在cdk-virtual-scroll-viewport内替代*ngFor支持与*ngFor完全相同的 API但有一个硬性前提viewport 必须设置itemSize属性固定尺寸策略。最简用法cdk-virtual-scroll-viewport itemSize50 classexample-viewport div *cdkVirtualForlet item of items classexample-item{{item}}/div /cdk-virtual-scroll-viewport完整可运行示例见 cdk-virtual-scroll-overview 示例官方文档中通过!-- example(cdk-virtual-scroll-overview) --内嵌展示。5.2 模板上下文变量*cdkVirtualFor会向模板暴露以下上下文变量见 virtual-for-of.ts 中CdkVirtualForOfContext的类型定义上下文变量说明$implicit当前条目值cdkVirtualForOf传入*cdkVirtualFor的 DataSource / Observable / 可迭代对象index条目在数据源中的索引count数据源条目总数first是否为数据源第一项last是否为数据源最后一项evenindex是否为偶数oddindex是否为奇数重要语义以上所有index/count/first/last/even/odd都基于数据源中的索引而非渲染子集中的位置。trackBy函数的index参数同样取数据源索引——实现上cdkVirtualForTrackBy的 setter 会对用户函数做包装fn(index renderedRange.start, item)virtual-for-of.ts。5.3 视图回收View Recycling*cdkVirtualFor内部使用_RecycleViewRepeaterStrategy滚动离开视口的视图不会立即销毁而是进入缓存下次需要新视图时优先复用缓存从而显著降低创建/销毁开销。缓存大小由cdkVirtualForTemplateCacheSize控制默认值为20个模板见 virtual-for-of.ts对应 API 报告中的get cdkVirtualForTemplateCacheSize设为0则完全禁用缓存若模板本身内存占用较大可适当调小该值以控制内存开销。5.4 数据来源Array、Observable 与 DataSource*cdkVirtualFor接受三类数据ArrayTObservableT[]DataSourceT—— 与 Table、Tree 组件共用同一抽象类只有connect与disconnect两个方法在 setter 实现中virtual-for-of.ts非 DataSource 输入会被统一包装为ArrayDataSourcedataStream则通过startWith(null)→pairwise()→switchMap(_changeDataSource)→shareReplay(1)完成「切换数据源时自动 disconnect 旧源、connect 新源、向后续订阅者重放最新数据」的管道。基于 DataSource 可实现分页按需加载——仓库中的 cdk-virtual-scroll-data-source 示例 演示了完整模式在connect()中订阅collectionViewer.viewChange即CdkVirtualForOf的viewChange: SubjectListRange根据{start, end}计算需要加载的页用setTimeout模拟 2001200ms 的服务器延迟后把数据写入BehaviorSubject并nextdisconnect()中退订所有订阅。六、固定尺寸策略itemSize / minBufferPx / maxBufferPx6.1 指令与默认值当所有条目尺寸相同时使用固定尺寸策略可获得最佳性能——渲染时无需测量条目。CdkFixedSizeVirtualScroll的选择器是cdk-virtual-scroll-viewport[itemSize]fixed-size-virtual-scroll.ts它通过工厂_fixedSizeVirtualScrollStrategyFactory把内部持有的FixedSizeVirtualScrollStrategy提供给VIRTUAL_SCROLL_STRATEGY注入令牌。三个输入的默认值与类型转换如下输入默认值类型语义itemSize20pxNumberInput经coerceNumberProperty转换每个条目的固定尺寸minBufferPx100pxNumberInput视口外必须保底渲染的最小缓冲像素低于此值立即补充渲染maxBufferPx200pxNumberInput检测到需要补充缓冲时一次性渲染回该缓冲像素量默认值见 fixed-size-virtual-scroll.ts。ngOnChanges中会调用_scrollStrategy.updateItemAndBufferSize同步更新策略该方法的开发模式下还会校验maxBufferPx minBufferPx否则抛出CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx。6.2 缓冲参数交互示例官方文档原例官方文档给出了参数为itemSize 50、minBufferPx 100、maxBufferPx 250时的行为推演用户滚动过程中视口检测到剩余缓冲只有90px90px 低于minBufferPx100px必须补充渲染至少渲染回maxBufferPx250px因此额外渲染 4 个条目4 × 50px 200px使缓冲总量达到290px重新高于maxBufferPx。对应源码在_updateRenderedRange中fixed-size-virtual-scroll.ts先按scrollOffset / itemSize计算firstVisibleIndex再分别判断首/尾缓冲是否低于minBufferPx用Math.ceil((maxBufferPx - buffer) / itemSize)计算需要扩展的条目数最终setRenderedRangesetRenderedContentOffset 发射scrolledIndexChange。它同时处理了「滚到底部后数据变短」的边界基于新数据长度与视口尺寸重算可见范围并纠正滚动偏移L134-L152。可运行示例cdk-virtual-scroll-fixed-buffer 示例10 万条数据 自定义缓冲参数。七、视口方向横向虚拟滚动视口默认垂直设置orientationhorizontal即可横向滚动。方向变化会在 setter 中触发_calculateSpacerSize()重算 spacer 的宽/高virtual-scroll-viewport.ts横向时把总尺寸写入width、纵向时写入height。同时组件 host 会追加cdk-virtual-scroll-orientation-horizontal/cdk-virtual-scroll-orientation-vertical类。切换方向后必须用 CSS 保证条目横向排布官方建议针对内容包装器.cdk-virtual-scroll-content-wrapper施加样式例如.cdk-virtual-scroll-content-wrapper { display: flex; flex-direction: row; }完整示例见 cdk-virtual-scroll-horizontal 示例其 CSS 即按此思路编写。八、有父元素标签限制的条目tr / li / dl某些 HTML 元素对父标签有硬性约束例如tr必须位于table内、li必须位于ul/ol内无法直接把cdk-virtual-scroll-viewport作为其父元素。官方给出的解决方案是先把条目放进合法父元素再整体包裹在cdk-virtual-scroll-viewport中cdk-virtual-scroll-viewport itemSize50 dl dt *cdkVirtualForlet item of items{{item}}/dt /dl /cdk-virtual-scroll-viewport官方文档同时给出警告父元素不得引入额外间距如margin、padding否则会干扰滚动度量。参考 cdk-virtual-scroll-dl 示例。九、自定义滚动策略VirtualScrollStrategy 与 VIRTUAL_SCROLL_STRATEGY视口确定「总内容多大、当前该渲染哪一段」完全依赖一个VirtualScrollStrategy。接口定义virtual-scroll-strategy.tsAPI 报告中的VirtualScrollStrategy包含export interface VirtualScrollStrategy { scrolledIndexChange: Observablenumber; attach(viewport: CdkVirtualScrollViewport): void; detach(): void; onContentScrolled(): void; onDataLengthChanged(): void; onContentRendered(): void; onRenderedOffsetChanged(): void; scrollToIndex(index: number, behavior: ScrollBehavior): void; }使用itemSize指令是最简单的接入方式若要提供自定义策略实现该接口并以VIRTUAL_SCROLL_STRATEGY令牌在包含视口的组件上提供即可。仓库文档提到支持不同尺寸条目的 autosize 策略正在angular/cdk-experimental中开发尚未达到生产可用。可参考 cdk-virtual-scroll-custom-strategy 示例 的写法。十、appendOnly 追加模式对于渲染成本较高的条目视口支持「只增不减」的appendOnly模式设为true后已渲染的视图即使滚出视口也会保留在 DOM 中virtual-scroll-viewport.ts。实现上setRenderedRange会强制把起始索引固定为 0、只扩大结束索引L350-L358setRenderedContentOffset也始终从顶部偏移 0 开始。适合「列表向下滚动只追加、无需回收」的场景参见 cdk-virtual-scroll-append-only 示例。十一、分离视口与滚动元素cdkVirtualScrollingElement 与 scrollWindow默认情况下视口自身就是滚动元素但有两种常见需求需要把滚动行为交给外部元素让视口的某个祖先元素滚动例如希望在虚拟化内容前后追加非虚拟化内容让整页一起滚动在该祖先元素上添加cdkVirtualScrollingElement指令对应CdkVirtualScrollableElement选择器[cdkVirtualScrollingElement]该元素会通过VIRTUAL_SCROLLABLE令牌提供给视口参考 cdk-virtual-scroll-parent-scrolling 示例。直接用浏览器窗口滚动移动端常见浏览器 chrome 可随滚动收起体验更好在cdk-virtual-scroll-viewport上加scrollWindow属性即可对应CdkVirtualScrollableWindow选择器cdk-virtual-scroll-viewport[scrollWindow]示例 HTML来自 cdk-virtual-scroll-window-scrolling 示例div classexample-headerContent before/div cdk-virtual-scroll-viewport scrollWindow itemSize50 div *cdkVirtualForlet item of items classexample-item{{item}}/div /cdk-virtual-scroll-viewport div classexample-footerContent after/div视口构造时会尝试inject(VIRTUAL_SCROLLABLE, {optional: true})若提供了外部滚动容器则直接使用否则给自身 host 追加cdk-virtual-scrollable类并把自己当作滚动容器virtual-scroll-viewport.ts。measureScrollOffset与measureViewportOffset等度量方法会据此在「自身」与「外部容器」之间切换测量目标L437-L479。十二、程序化控制滚动、度量与状态读取 APICdkVirtualScrollViewport提供了一组供业务代码直接调用的方法均见 virtual-scroll-viewport.ts方法说明scrollToIndex(index, behavior auto)滚动到指定索引处委托给策略固定尺寸策略实现为index * itemSizescrollToOffset(offset, behavior auto)滚动到距视口起点的指定像素偏移横向 RTL 下会换算为「虚拟 scrollRight」语义checkViewportSize()重新度量视口尺寸并触发onDataLengthChanged视口尺寸变化时自动调用getDataLength()/getViewportSize()/getRenderedRange()读取数据长度、视口尺寸、当前渲染区间measureScrollOffset(from?)/measureViewportOffset(from?)度量滚动偏移 / 视口相对滚动容器的偏移支持top/left/right/bottom/start/endmeasureRenderedContentSize()/measureRangeSize(range)度量已渲染内容总尺寸 / 指定区间尺寸getOffsetToRenderedContentStart()渲染内容起点距视口起点的偏移to-end模式下下一渲染周期前返回nullattach(forOf)/detach()挂接/解绑CdkVirtualScrollRepeater由CdkVirtualForOf内部调用CdkVirtualScrollRepeaterT接口只要求两个成员dataStream: Observablereadonly T[]与measureRangeSize(range, orientation)CdkVirtualForOf即其标准实现。所有对dataStream的订阅如视口监听数据长度变化都在 zone 外执行。十三、总结与进一步阅读angular/cdk/scrolling以「滚动基础设施 虚拟滚动」双线并进CdkScrollableScrollDispatcher负责把分散的滚动事件收敛为可复用的 Observable 流ViewportRuler提供视口度量cdk-virtual-scroll-viewport*cdkVirtualForFixedSizeVirtualScrollStrategy通过 spacer 撑高、transform 平移与视图回收让十万级数据列表也能流畅滚动。关键默认值速查itemSize 20、minBufferPx 100、maxBufferPx 200、cdkVirtualForTemplateCacheSize 20、DEFAULT_SCROLL_TIME 20、DEFAULT_RESIZE_TIME 20单位均为 ms/px。进一步阅读建议官方使用文档src/cdk/scrolling/scrolling.md含!-- example(...) --内嵌的全部示例引用完整的 API 报告goldens/cdk/scrolling/index.api.md源码实现src/cdk/scrolling建议按virtual-scroll-viewport.ts→fixed-size-virtual-scroll.ts→virtual-for-of.ts顺序阅读可运行示例src/components-examples/cdk/scrolling覆盖 overview、context、template-cache、data-source、fixed-buffer、horizontal、dl、custom-strategy、append-only、parent-scrolling、window-scrolling 共 11 个场景测试佐证virtual-scroll-viewport.spec.ts、scroll-dispatcher.spec.ts、viewport-ruler.spec.ts、scrollable.spec.ts 等【免费下载链接】componentsComponent infrastructure and Material Design components for Angular项目地址: https://gitcode.com/GitHub_Trending/co/components创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考