对 scroll 的认知和探索
一、基础回顾:测量元素的尺寸和位置
由于scroll中会涉及到元素的尺寸和位置的计算,这里先借助网上的一张图来回顾一下。
图片来源自: 使用 CSSOM 测量元素尺寸和位置 。
clientHeight
需要注意的是: clientHeight = 内容高度+上下padding-滚动条的粗度。 对于同一个元素来说,盒模型会影响clientHeight的值。 clientHeight返回的是整数。
#scroll-container2 {
margin: 100px 0px 70px 50px;
border: 7px solid black;
box-sizing: border-box; // box-sizing的值会影响clientHeight、clientWidth的值
height: 200.22px;
width: 300.22px;
padding: 10px;
overflow: auto;
}
// 备注:该示例里没有水平滚动条
getClientWidth() {
/**
* (1)对于content-box,CSS height属性只包含内容的高度,不包含padding和border。
* 内容高度contentHeight = 200.22
* clientHeight = CSS height + CSS 上下padding = 200.22+10+10=220.22 clientHeight会返回整数 220
*
* (2)对于border-box,CSS height属性包含了内容的高度,padding和border。
* 内容高度contentHeight = 200.22(CSS height)-7(上border)-7(下border)-10(上padding)-10(下padding)=166.22。
* clientHeight = 内容height+上下padding = 166.22+10+10=186.22 clientHeight会返回整数 186
*/
const scrollContainer = document.getElementById('scroll-container2')!;
console.log('clientHeight', scrollContainer?.clientHeight);
}
二、滚动的相关概念
- 当块级元素设置了
overflow: scroll
或overflow: auto
,并且它包含的内容溢出了其有限的可视区时,就会显示滚动条,内容可滚动。 - 我们可以监听滚动事件,监听元素滚动到特定位置时做我们想做的事情。
- 我们也可以手动设置可滚动元素滚动指定的偏移量,比如滚动页面让指定元素进入可视区。
一些无关紧要的示例代码:
<div style="display: flex">
<div id="scroll-container1">内容很少,没有滚动条</div>
<div id="scroll-container2">
01. 内容很多,有滚动条<br />
02. 内容很多,有滚动条<br />
03. 内容很多,有滚动条<br />
04. 内容很多,有滚动条<br />
05. 内容很多,有滚动条<br />
06. 内容很多,有滚动条<br />
07. 内容很多,有滚动条<br />
08. 内容很多,有滚动条<br />
09. 内容很多,有滚动条<br />
10. 内容很多,有滚动条<br />
11. 内容很多,有滚动条<br />
12. 内容很多,有滚动条<br />
13. 内容很多,有滚动条<br />
14. 内容很多,有滚动条<br />
15. 内容很多,有滚动条<br />
<div id="target">test scrollIntoView()</div>
01. 内容很多,有滚动条<br />
02. 内容很多,有滚动条<br />
03. 内容很多,有滚动条<br />
04. 内容很多,有滚动条<br />
05. 内容很多,有滚动条<br />
06. 内容很多,有滚动条<br />
07. 内容很多,有滚动条<br />
08. 内容很多,有滚动条<br />
09. 内容很多,有滚动条<br />
10. 内容很多,有滚动条<br />
11. 内容很多,有滚动条<br />
12. 内容很多,有滚动条<br />
13. 内容很多,有滚动条<br />
14. 内容很多,有滚动条<br />
15. 内容很多,有滚动条<br />
</div>
</div>
#scroll-container1,
#scroll-container2 {
margin: 100px 0px 70px 50px;
border: 7px solid black;
box-sizing: border-box; // content-box
height: 200.22px;
width: 300.22px;
padding: 10px;
overflow: scroll; // auto
}
#target {
height: 50px;
padding: 10px;
background-color: bisque;
border: 1px solid black;
}
三、监听滚动事件
当用户滚动某个元素的内容时 scroll 事件将会被触发。
由于 scroll 事件可被高频触发,容易造成高性能的消耗。推荐使用 requestAnimationFrame()
、 setTimeout()
或 CustomEvent
给事件节流,比如:
const scrollContainer = document.getElementById('scroll-container')!;
scrollContainer.addEventListener('scroll', () => {
requestAnimationFrame(() => {
// do something here
console.log('我滚动啦');
});
});
显示器有固定的刷新频率,每秒最多只能重绘60次或75次,即60Hz或75Hz。requestAnimationFrame 充分利用显示器的刷新机制,与这个刷新频率保持同步,来进行页面重绘,从而节省系统资源,提高系统性能,改善视觉效果。
四、滚动的原生API
1. scroll()、scrollTo()、scrollBy()
element.scroll(x-coord, y-coord)
:让滚动条滚动到指定容器的某个坐标。
element.scroll(options)
:让滚动条滚动到指定容器的某个坐标,且可以指定滚动行为。
element.scrollTo() 同 element.scroll()。
const scrollContainer = document.getElementById('scroll-container')!;
// 1. 内容垂直方向向上滚动50px
scrollContainer.scroll(0, 50);
// 2. 内容垂直方向向上滚动50px
scrollContainer?.scroll({
top: 50,
left: 0,
behavior: 'smooth', // 指定滚动行为,支持参数 smooth(平滑滚动),默认值auto(瞬间滚动)
});
scroll、scrollTo、scrollBy 接收参数一样。区别:
scroll 和 scrollTo 用法基本一致。
scrollTo
滚动的距离是 绝对
的,就是不管执行多少次,滚动的位置都是一样的。
scrollBy
滚动的距离是 相对
的,每执行一次就会在原来的滚动基础上加上相对的距离。
2. scrollIntoView()
滚动element的父容器,使element对用户可见。
element.scrollIntoView(alignToTop)
:alignToTop可选。
- 默认值为true。element的顶端将与其所在滚动区的可视区域的顶端对齐。(特殊的,如果element在其所在滚动区的可视区域的最底部,则element将与其所在滚动区的可视区域的底端对齐。)
- 如果为false,element的底端将与其所在滚动区的可视区域的底端对齐。
element.scrollIntoView(scrollIntoViewOptions)
:scrollIntoViewOptions对象可选。
- behavior:滚动动画过渡效果,值可为auto,smooth。smooth表示平滑滚动。
- block:垂直方向的对齐,值可为start,center(滚动到可视区域中间),end,nearest。默认为 start。
- inline:水平方向的对齐, 值可为start,center,end,nearest。默认为 nearest。
// 滚动son的父容器,使son出现在可视区域。
const son = document.getElementById('son')!;
son.scrollIntoView();
3. scrollTop、scrollLeft
element.scrollTop
:读取或设置一个元素的内容垂直滚动的距离。这个距离是元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离。
element.scrollLeft
:读取或设置元素滚动条到元素左边的距离。
// 获取
const scrollTop = scrollContainer.scrollTop;
console.log('scrollTop', scrollTop);
// 设置
scrollContainer.scrollTop = 77;
4. scrollHeight、scrollWidth
element.scrollHeight
:element的内容高度,包括由于溢出导致的视图中不可见内容。没有垂直滚动条的情况下,scrollHeight 值与元素视图填充所有内容所需要的最小值 clientHeight 相同。scrollHeight 也包括 ::before
和 ::after
这样的伪元素。
注意:scrollHeight将会对值取整。如果需要小数值,使用 Element.getBoundingClientRect() 。
5. scrollY、scrollX
window.scrollY
返回文档在垂直方向已滚动的像素值。
window.scrollX
返回文档在水平方向已滚动的像素值。
为了跨浏览器兼容,请使用 window.pageYOffset
代替 window.scrollY
。
window.pageYOffset == window.scrollY; // 总是返回 true
6. 性能问题
需要注意的是,上述的滚动相关的API都会触发强制的回流。 相关概念:
- 在修改元素的外观,比如background-color,会触发
重绘
。 - 在获取或设置元素在视口中的位置或大小时,都会触发
回流(布局抖动)
。 回流一定会引起重绘
。(因为在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程即是重绘)。重绘不一定会引起回流
。
五、滚动的应用
1. 判断元素是否完全出现在目标可视区?
方案一:使用getBoundingClientRect方法,但是每次调用时都会触发回流,严重影响性能。即使使用节流函数来控制也依然很影响性能。不推荐。
const target = document.getElementById('target')!;
const scrollContainer = document.getElementById('scroll-container')!;
scrollContainer.addEventListener('scroll', () => {
const targetRect = target.getBoundingClientRect();
const scrollContainerRect = scrollContainer.getBoundingClientRect();
const isTargetAllInScrollContainer =
targetRect.left > scrollContainerRect.left &&
targetRect.right < scrollContainerRect.right &&
targetRect.top > scrollContainerRect.top &&
targetRect.bottom < scrollContainerRect.bottom;
if (isTargetAllInScrollContainer) {
console.log('目标元素完全出现在目标容器中');
} else {
console.log('目标元素没有完全出现在目标容器中');
}
});
方案二:使用 IntersectionObserver 交叉观察者。推荐。
const target = document.getElementById('target')!;
const scrollContainer = document.getElementById('scroll-container')!;
const callback = (entries: IntersectionObserverEntry[]) => {
console.log('entries: ', entries);
if (entries[0].intersectionRatio === 1) {
// 目标元素有一点点在父容器的padding上都不算,必须完全出现在内容区
console.log('目标元素完全出现在目标容器的可视区域中');
} else {
console.log('目标元素没有完全出现在目标容器的可视区域中');
}
};
const options = {
root: scrollContainer,
rootMargin: '0px',
threshold: 1,
};
const observer = new IntersectionObserver(callback, options);
observer.observe(target);
2. 滚动到指定元素的方案有哪些?
方案一:使用 element.scrollTo(ScrollToOptions)
让目标元素滚动到指定坐标点。
方案二:位置计算+设置scrollTop
,比如我们组件库的 ScrollToService.scrollToElement() 。
方案三:使用 scrollIntoView(scrollIntoViewOptions)
让目标元素滚动到目标可视区的顶部、中央或底部。
方案四:通过点击锚链接 <a href="#target">跳转到指定锚点</a>
跳转到指定锚点。
六、修改滚动条样式
::-webkit-scrollbar
CSS 伪类元素会影响设置了 overflow:scroll;
的元素的滚动条样式。通过以下选择器,我们可以修改滚动条或滚动槽的宽高颜色等样式。滚动条选择器有下面这些:
::-webkit-scrollbar
设置整个滚动条的样式,比如修改height或width等。
::-webkit-scrollbar-button
滚动条上的按钮(上下箭头)。
::-webkit-scrollbar-thumb
滚动条上的滚动滑块。
::-webkit-scrollbar-track
滚动条轨道。
::-webkit-scrollbar-track-piece
滚动条没有滑块的轨道部分。
::-webkit-resizer
出现在某些元素底角的可拖动调整大小的滑块。
::-webkit-scrollbar-corner
当同时有垂直滚动条和水平滚动条时交汇的部分。通常是浏览器窗口的右下角。
七、scroll和wheel的区别
scroll
事件在滚动条滚动时被触发。
wheel
事件在鼠标滚轮滚动的时候被触发。
八、Angular CDK 之 scrolling
1. Map "字典“
Map基础回顾:
/**
* new Map()
* Map是一种叫做字典的数据结构。
* 它的特点是键值对的形式,键可以是任意类型,值也可以是任意类型。
*/
const map = new Map();
/**
* set()
* 设置键名key对应的键值为value,然后返回整个 Map 结构。
* 如果key已经有值,则键值会被更新,否则就新生成该键。
* 同时返回的是当前Map对象,可采用链式写法。
*/
map.set('第一站', '故宫');
map
.set('第二站', '奥森公园')
.set('第三站', '香山公园')
.set('第四站', '长城')
.set('第五站', '颐和园');
/**
* Map(5) {'第一站' => '故宫',
* '第二站' => '奥森公园', '第三站' => '香山公园',
* '第四站' => '长城', '第五站' => '颐和园'}
*/
console.log(map);
// get() 读取key对应的键值,如果找不到key,返回undefined。
const getValue = map.get('第三站'); // 香山公园
// size 返回Map结构的成员总数。
const size = map.size; // 5
// has() 返回一个布尔值,判断某个键是否在当前Map字典中。
const hasName = map.has('第二站'); // true
// forEach((value, key) => {}); 遍历
map.forEach((value, key) => {
console.log('value', value);
console.log('key', key);
});
// delete() 删除某个键,返回true。如果删除失败,返回false。
const hasDelete = map.delete('第五站'); // true
// clear() 清除所有成员,没有返回值。
map.clear(); // Map(0) {size: 0}
2. 不同浏览器处理水平滚动的方式
当dir属性值为'rtl',即文本方向是从右到左时。各个浏览器对 scrollLeft 的取值方式不同。如下:
示例一:观察到不同浏览器在以下版本下在RTL布局中读取scrollLeft属性值。
<div id="scroll-container2" [dir]="'rtl'">
<div id="target" style="width: 500px">test 水平滚动条</div>
01. 内容很多,有滚动条<br />
02. 内容很多,有滚动条<br />
03. 内容很多,有滚动条<br />
04. 内容很多,有滚动条<br />
05. 内容很多,有滚动条<br />
06. 内容很多,有滚动条<br />
07. 内容很多,有滚动条<br />
08. 内容很多,有滚动条<br />
09. 内容很多,有滚动条<br />
10. 内容很多,有滚动条<br />
11. 内容很多,有滚动条<br />
12. 内容很多,有滚动条<br />
13. 内容很多,有滚动条<br />
14. 内容很多,有滚动条<br />
15. 内容很多,有滚动条<br />
</div>
#scroll-container2 {
margin: 100px 0px 70px 50px;
border: 7px solid black;
box-sizing: content-box; // box-sizing的值会影响clientHeight、clientWidth的值
height: 200.22px;
width: 300.22px;
padding: 10px;
overflow: auto;
}
#target {
height: 50px;
padding: 10px;
background-color: bisque;
border: 1px solid black;
}
所以上述对应版本的浏览器都使用了 NEGATED 的方式来读取scrollLeft值的???
示例二:验证RTL布局下,NEGATED浏览器中,让容器的滚动条滚动一定偏移量;读取scrollLeft的值;读取容器的左右两侧的偏移量。
<div id="scroll-container2" [dir]="'rtl'">
<div id="target" style="width: 500px">test 水平滚动条</div>
01. 内容很多,有滚动条<br />
02. 内容很多,有滚动条<br />
03. 内容很多,有滚动条<br />
04. 内容很多,有滚动条<br />
05. 内容很多,有滚动条<br />
06. 内容很多,有滚动条<br />
07. 内容很多,有滚动条<br />
08. 内容很多,有滚动条<br />
09. 内容很多,有滚动条<br />
10. 内容很多,有滚动条<br />
11. 内容很多,有滚动条<br />
12. 内容很多,有滚动条<br />
13. 内容很多,有滚动条<br />
14. 内容很多,有滚动条<br />
15. 内容很多,有滚动条<br />
</div>
const scrollContainer = document.getElementById('scroll-container2')!;
console.log('scrollLeft: ', scrollContainer.scrollLeft);
RTL布局中,默认水平滚动条会停留在右侧。读取到的scrollLeft值是0。
让水平滚动条向左滚动:
// RTL布局下,NEGATED浏览器中,scrollLeft的值是负数或0。
// scrollLeft的绝对值等于容器右侧的偏移量
scrollContainer.scrollLeft = -100;
读取该容器的左侧和右侧的偏移量:
console.log('容器右侧的偏移量: ', -scrollContainer.scrollLeft); // = -(-100) = 100
console.log('容器左侧的偏移量:', scrollWidth - clientWidth - Math.abs(scrollLeft));
// 等于 scrollWidth - clientWidth - (-scrollLeft) 即是 542-271-(-(-100))
// 等于 scrollWidth - clientWidth + scrollLeft 即是 542-271+(-100) = 171
3. CdkScrollable 指令
第一,给元素绑定cdkScrollable指令后,该scrollable实例会注册到ScrollDispatcher的scrollContainers”字典“中。当cdkScrollable指令被销毁后,该scrollable实例也将从scrollDispatcher的scrollContainers”字典“中移除,对应的滚动事件订阅也会被取消。
ngOnInit() {
this.scrollDispatcher.register(this);
}
ngOnDestroy() {
this.scrollDispatcher.deregister(this);
}
第二,cdkScrollable指令的 elementScrolled 方法是一个可观察者 Observable ,它观察cdkScrollable指令绑定的元素的滚动事件。我们可以通过该方法来订阅滚动事件并做我们想做的事情。
elementScrolled(): Observable<Event> {
return new Observable((observer: Observer<Event>) =>
this.ngZone.runOutsideAngular(() =>
fromEvent(this.elementRef.nativeElement, 'scroll')
.pipe(takeUntil(this._destroyed))
.subscribe(observer),
),
);
}
第三,如果我们自己使用原生的scrollTo来进行滚动指定偏移量的话,我们只能设置top和left偏移。在一些复杂的场景下(比如不同的文本布局下,不同的浏览器下),我们只能自己去计算这个top或left值,会比较麻烦和繁琐。
// 原生scrollTo
element.scrollTo({
top: number,
left: number,
behavior: 'smooth'|'auto',
});
cdkScrollable 指令给我们提供了更强大的 scrollTo(ExtendedScrollToOptions) 方法,它扩展了ScrollToOptions,支持我们传上、下、左、右、开始和结束的偏移量。
start和end的含义:在LTR布局中是左(start)和右(end)。在RTL布局中是右(start)和左(end)。
由于浏览器对于滚动 scrollLeft 在 RTL 中的含义并不一致,CdkScrollable的scrollTo方法对浏览器原生scrollTo方法进行了规范化:left 和 right 总是指滚动容器的左侧和右侧,而与布局方向无关。
// CdkScrollable的scrollTo
scrollTo(options: {
top?: number;
bottom?: number;
left?: number;
right?: number;
start?: number;
end?: number;
behavior?: 'auto' | 'smooth';
}): void {
// 复杂逻辑判断......
}
它里面进行了哪些复杂逻辑的判断和处理??? 经过梳理发现,它做了三件事: 第一件事:计算出顶部偏移量top。
if (options.bottom != null) {
options.top = el.scrollHeight - el.clientHeight - options.bottom;
}
第二件事:计算左偏移left。 兼容不同文本方向(ltr和rtl),同时兼容不同浏览器对scrollLeft的不同处理。
//(1)根据dir文本方向以及start和end的值,计算出left、right偏移量
if (options.left == null) {
options.left = isRtl ? options.end : options.start;
}
if (options.right == null) {
options.right = isRtl ? options.start : options.end;
}
// (2)兼容各个浏览器,因为浏览器对于滚动scrollLeft在 RTL 中的含义并不一致。
if (isRtl && getRtlScrollAxisType() != RtlScrollAxisType.NORMAL) {
// 兼容dir='rtl'的情况
if (options.left != null) {
options.right = el.scrollWidth - el.clientWidth - options.left;
}
if (getRtlScrollAxisType() == RtlScrollAxisType.INVERTED) {
// 兼容 INVERTED 浏览器
options.left = options.right;
} else if (getRtlScrollAxisType() == RtlScrollAxisType.NEGATED) {
// 兼容 NEGATED 浏览器
options.left = options.right ? -options.right : options.right;
}
} else {
// dir='ltr'的情况 或者 NORMAL 浏览器中
if (options.right != null) {
options.left = el.scrollWidth - el.clientWidth - options.right;
}
}
第三件事:调用原生的scrollTo方法或者设置scrollTop、scrollLeft来实现滚动。
this._applyScrollToOptions(options);
private _applyScrollToOptions(options: ScrollToOptions): void {
const el = this.elementRef.nativeElement;
if (supportsScrollBehavior()) {
el.scrollTo(options);
} else {
if (options.top != null) {
el.scrollTop = options.top;
}
if (options.left != null) {
el.scrollLeft = options.left;
}
}
}
第四,cdkScrollable 指令给我们提供了 测量相对于视口指定边缘的滚动偏移量
的方法。可以使用此方法代替直接检查 scrollLeft 或 scrollTop,因为浏览器对于滚动的scrollLeft在 RTL 中的含义并不一致。此方法返回的值被规范化,这样无论布局方向如何,from的 left 和 right 总是指滚动容器的左侧和右侧。
measureScrollOffset(from: 'top' | 'left' | 'right' | 'bottom' | 'start' | 'end'): number {
// from 指定想要返回容器的哪一侧的偏移量。
const LEFT = 'left';
const RIGHT = 'right';
const el = this.elementRef.nativeElement;
if (from == 'top') {
// 返回容器上侧的偏移量。
return el.scrollTop;
}
if (from == 'bottom') {
// 返回容器下侧的偏移量。
return el.scrollHeight - el.clientHeight - el.scrollTop;
}
// ⭐️⭐️⭐️ 根据文本方向以及start和end的值来确定想要返回的是容器左侧还是右侧的偏移量。
const isRtl = this.dir && this.dir.value == 'rtl';
if (from == 'start') {
// 如果是LTR布局,start表示想要返回容器左侧的偏移量,from=left。如果是RTL布局,start表示想要返回容器右侧的偏移量,from=right。
from = isRtl ? RIGHT : LEFT;
} else if (from == 'end') {
// 如果是LTR布局,end表示想要返回容器右侧的偏移量,from=right。如果是RTL布局,end表示想要返回容器左侧的偏移量,from=left。
from = isRtl ? LEFT : RIGHT;
}
// ⭐️⭐️⭐️ 根据不同浏览器在RTL情况下对scrollLeft的处理方式不同,来计算确定返回容器左侧或者右侧的偏移量。
if (isRtl && getRtlScrollAxisType() == RtlScrollAxisType.INVERTED) {
// 在RTL布局中,对于 INVERTED,当一直向左滚动时,scrollLeft 为 (scrollWidth - clientWidth),一直向右滚动时为 0。
// 这里el.scrollLeft是正数,值等于容器的右侧偏移量。✅
if (from == LEFT) {
return el.scrollWidth - el.clientWidth - el.scrollLeft;
} else {
return el.scrollLeft;
}
} else if (isRtl && getRtlScrollAxisType() == RtlScrollAxisType.NEGATED) {
// 在RTL布局中,对于 NEGATED,scrollLeft 在一直向左滚动时为 -(scrollWidth - clientWidth),在一直向右滚动时为 0。
// 这里el.scrollLeft是负数,绝对值等于容器的右侧偏移量。✅
if (from == LEFT) {
return el.scrollLeft + el.scrollWidth - el.clientWidth;
} else {
return -el.scrollLeft;
}
} else {
// 对于 NORMAL 以及非 RTL 布局,当一直向左滚动时 scrollLeft 为 0,当一直向右滚动时为 (scrollWidth - clientWidth)。
// 这里el.scrollLeft是正数,值等于容器的左侧偏移量。✅
if (from == LEFT) {
return el.scrollLeft;
} else {
return el.scrollWidth - el.clientWidth - el.scrollLeft;
}
}
}
}
第五,cdkScrollable 指令提供了getElementRef() 方法供我们获取该可滚动元素的引用。
// 获取该可滚动元素的引用。
getElementRef(): ElementRef<HTMLElement> {
return this.elementRef;
}
总的来说,CdkScrollable 把文本的布局和浏览器的兼容性问题都给我们考虑并处理好了。
- 当你想获取该可滚动元素的引用?
调用
cdkScrollable.getElementRef()
即可。 - 当你想监听该可滚动元素的滚动事件?
调用
cdkScrollable.elementScrolled()
进行订阅即可。 - 当你想让元素滚动到指定位置?
调用
cdkScrollable.scrollTo(ExtendedScrollToOptions)
即可。 - 当你想测量滚动的偏移量?
调用
cdkScrollable.measureScrollOffset(from)
即可。
4. ScrollDispatcher 服务
第一,scrollDispatcher里维护了一本装有 [scrollable引用]及其[scroll事件订阅]的映射 的字典,该字典名为 scrollContainers 。
scrollContainers: Map<CdkScrollable, Subscription> = new Map();
第二,scrollDispatcher里会有一个通知“中转站” _scrolled,它即是一个observable(可观察者),也是一个 observer(观察者)。当有元素发生滚动时,它会发出“消息”。同时也可以通过它订阅元素的滚动”消息“。 当cdkScrollable绑定的元素发生滚动时,_scrolled会发射该scrollable实例。当全局监听的元素发生滚动时,它也会发射但不发任何东西。它会昭告天下:有元素发生了滚动。
_scrolled = new Subject<CdkScrollable | void>();
第三,当元素上绑定了CdkScrollable指令,该指令会调用scrollDispatcher的register方法,将该scrollable引用及它对应的滚动事件订阅作为键值对注册到scrollContainers字典中。
register(scrollable: CdkScrollable): void {
if (!this.scrollContainers.has(scrollable)) {
this.scrollContainers.set(
scrollable,
scrollable.elementScrolled().subscribe(
// 在scrollable元素发生滚动时,_scrolled发出该scrollable实例
// Mark A: 这里监听的是当前的scrollable实例的滚动,_scrolled会发射该scrollable实例。
() => this._scrolled.next(scrollable)
),
);
}
}
第四,当CdkScrollable指令被销毁,scrollDispatcher会根据scrollable引用,拿到字典中对应的滚动订阅并取消它,同时从字典中删除该映射。
deregister(scrollable: CdkScrollable): void {
const scrollableReference = this.scrollContainers.get(scrollable);
if (scrollableReference) {
scrollableReference.unsubscribe();
this.scrollContainers.delete(scrollable);
}
}
第五, getAncestorScrollContainers():CdkScrollable[]
方法会从scrollContainers字典中找到所有包含所提供元素的scrollable并返回。
getAncestorScrollContainers(elementOrElementRef: ElementRef | HTMLElement): CdkScrollable[] {
const scrollingContainers: CdkScrollable[] = [];
this.scrollContainers.forEach((_subscription: Subscription, scrollable: CdkScrollable) => {
if (this._scrollableContainsElement(scrollable, elementOrElementRef)) {
scrollingContainers.push(scrollable);
}
});
return scrollingContainers;
}
第六, scrolled(): Observable<CdkScrollable | void>
方法里订阅了任何已注册的Scrollable引用(或 window, document, 或 body)触发的滚动事件并返回一个可观察者。
scrolled(auditTimeInMs: number = DEFAULT_SCROLL_TIME): Observable<CdkScrollable | void> {
return new Observable((observer: Observer<CdkScrollable | void>) => {
if (!this._globalSubscription) {
this._globalSubscription = this._ngZone.runOutsideAngular(() => {
const window = this._getWindow();
// Mark A: 这里监听的是全局元素的滚动,_scrolled发射了但没有发射任何东西。
return fromEvent(window.document, 'scroll').subscribe(() => this._scrolled.next());
});
}
// 订阅全局的scroll事件,观察者observer会在这里“盯着”_scrolled发射的内容并做想做的事情。
const subscription =
auditTimeInMs > 0
? this._scrolled.pipe(auditTime(auditTimeInMs)).subscribe(observer)
: this._scrolled.subscribe(observer);
this._scrolledCount++;
return () => {
subscription.unsubscribe();
this._scrolledCount--;
if (!this._scrolledCount) {
if (this._globalSubscription) {
// 取消全局监听scroll事件的订阅
this._globalSubscription.unsubscribe();
this._globalSubscription = null;
}
}
};
});
}
第七, ancestorScrolled(): Observable<CdkScrollable | void>
方法:订阅指定元素的祖先的滚动事件并返回一个可观察者。
ancestorScrolled(
elementOrElementRef: ElementRef | HTMLElement, // 指定要观察谁的祖先
auditTimeInMs?: number, // 指定滚动事件节流(throttle)的时间
): Observable<CdkScrollable | void> { // 返回一个可观察者,具体内容是祖先scrollable实例
const ancestors = this.getAncestorScrollContainers(elementOrElementRef);
return this.scrolled(auditTimeInMs).pipe(
filter(target => {
// Mark A: target有值表示的是cdkScrollable元素滚动触发的。
// Mark A: target为空则表示是全局监听到的元素的滚动。
return !target || ancestors.indexOf(target) > -1;
}),
);
}
5. scrolling工具类
supportsScrollBehavior():boolean
判断元素是否支持滚动行为。
getRtlScrollAxisType(): RtlScrollAxisType
获取当前浏览器对于RTL布局下的scrollLeft的取值方式 NORMAL
、NEGATED
或 INVERTED
。
九、参考学习
转载自:https://juejin.cn/post/7161420863055593479