要点
秘诀:您的下一个应用中可能不需要 scroll
事件。使用
IntersectionObserver
,
我将介绍如何在 position:sticky
元素固定或停止固定时触发自定义事件。所有这些都没有
滚动监听器的使用我们甚至提供了一个很棒的演示来证明这一点:
sticky-change
活动闪亮登场
使用 CSS 粘性位置的一个实际限制是 未提供平台信号来判断媒体资源何时处于启用状态。 换言之,没有事件可以知道某个元素是何时变为粘性的, 它就会不再固定
以下示例修复了 <div class="sticky">
距离事件 10px
父级容器的顶部:
.sticky {
position: sticky;
top: 10px;
}
如果浏览器在元素遇到标记时发出通知,不是很好吗?
显然我不是唯一一个
是这样的position:sticky
的信号可解锁许多用例:
- 当横幅固定时,为其应用阴影。
- 当用户阅读您的内容时,记录分析命中数据以了解他们的 进度。
- 当用户滚动页面时,将浮动 TOC widget 更新为当前 部分。
考虑到这些使用情形,我们制定了一个最终目标:创建一个
在 position:sticky
元素变得固定时触发。将其命名为
sticky-change
事件:
document.addEventListener('sticky-change', e => {
const header = e.detail.target; // header became sticky or stopped sticking.
const sticking = e.detail.stuck; // true when header is sticky.
header.classList.toggle('shadow', sticking); // add drop shadow when sticking.
document.querySelector('.who-is-sticking').textContent = header.textContent;
});
此演示使用 此事件在修复后为其添加阴影。它还更新了 新标题。
<ph type="x-smartling-placeholder">滚动效果不使用滚动事件?
<ph type="x-smartling-placeholder">我们先去解释一些术语,以便引用这些名称 完整说明:
- 滚动容器 - 包含 “博文”列表。
- Headers - 包含
position:sticky
的每个部分的蓝色标题。 - 粘性版块 - 每个内容版块。滚动至 粘性标题。
- “粘滞模式”- 将
position:sticky
应用于元素时。
为了知道哪个标头进入“粘性模式”,我们需要通过某种方式
滚动容器的滚动偏移量。这样我们就能
来计算当前显示的标题。不过,
在没有 scroll
事件的情况下很难做到这一点 :) 另一个问题是,
position:sticky
会在元素固定后从布局中移除该元素。
如果没有滚动事件,我们就无法执行与布局相关的 计算。
添加 dumby DOM 以确定滚动位置
我们将使用 IntersectionObserver
而不是 scroll
事件来
确定标头何时进入和退出粘性模式。添加两个节点
(也称为哨兵)在每个粘性部分中,一个在顶部,另一个在
将用作确定滚动位置的航点。由于这些
当标记进入和离开容器时,它们的可见性会发生变化,
Intersection Observer 会触发回调。
我们需要两个标记来涵盖向上和向下滚动的四种情况:
- 向下滚动 - 当顶部标记越过标题时,标题会变为粘性 容器顶部
- 向下滚动 - 标题会在贴靠到底部时退出粘滞模式 这部分及其底部标记与容器顶部相交。
- 向上滚动 - 标题会在其顶部的标记滚动时退出粘性模式 重新回到视野范围内
- 向上滚动 - 底部标记越回,标头就会变得粘滞 将镜头推入视野范围内
按出现顺序查看 1-4 的抓屏会很有帮助:
<ph type="x-smartling-placeholder">CSS
哨兵放置于每个部分的顶部和底部。
.sticky_sentinel--top
位于页眉的顶部,同时
.sticky_sentinel--bottom
位于该部分底部:
:root {
--default-padding: 16px;
--header-height: 80px;
}
.sticky {
position: sticky;
top: 10px; /* adjust sentinel height/positioning based on this position. */
height: var(--header-height);
padding: 0 var(--default-padding);
}
.sticky_sentinel {
position: absolute;
left: 0;
right: 0; /* needs dimensions */
visibility: hidden;
}
.sticky_sentinel--top {
/* Adjust the height and top values based on your on your sticky top position.
e.g. make the height bigger and adjust the top so observeHeaders()'s
IntersectionObserver fires as soon as the bottom of the sentinel crosses the
top of the intersection container. */
height: 40px;
top: -24px;
}
.sticky_sentinel--bottom {
/* Height should match the top of the header when it's at the bottom of the
intersection container. */
height: calc(var(--header-height) + var(--default-padding));
bottom: 0;
}
设置 Intersection Observer
Intersection Observer 以异步方式观察 目标元素和文档视口或父级容器。在本例中, 我们观察到与父级容器的交集。
魔法酱是 IntersectionObserver
。每个哨兵都有
IntersectionObserver
,用于观察其在
滚动容器。当标记滚动至可见视口时,
头文件被修复或不再具有粘性。同样,当某个标记离开
视口
首先,我为页眉和页脚哨兵设置了观察器:
/**
* Notifies when elements w/ the `sticky` class begin to stick or stop sticking.
* Note: the elements should be children of `container`.
* @param {!Element} container
*/
function observeStickyHeaderChanges(container) {
observeHeaders(container);
observeFooters(container);
}
observeStickyHeaderChanges(document.querySelector('#scroll-container'));
然后,我添加了一个观察器,以便在 .sticky_sentinel--top
元素传递时触发
(沿任一方向)穿过滚动容器顶部。
observeHeaders
函数会创建热门标记并将其添加到
每个部分。观察者会计算标记点与
并决定是进入还是离开视口。这样
信息决定了章节标题是否固定。
/**
* Sets up an intersection observer to notify when elements with the class
* `.sticky_sentinel--top` become visible/invisible at the top of the container.
* @param {!Element} container
*/
function observeHeaders(container) {
const observer = new IntersectionObserver((records, observer) => {
for (const record of records) {
const targetInfo = record.boundingClientRect;
const stickyTarget = record.target.parentElement.querySelector('.sticky');
const rootBoundsInfo = record.rootBounds;
// Started sticking.
if (targetInfo.bottom < rootBoundsInfo.top) {
fireEvent(true, stickyTarget);
}
// Stopped sticking.
if (targetInfo.bottom >= rootBoundsInfo.top &&
targetInfo.bottom < rootBoundsInfo.bottom) {
fireEvent(false, stickyTarget);
}
}
}, {threshold: [0], root: container});
// Add the top sentinels to each section and attach an observer.
const sentinels = addSentinels(container, 'sticky_sentinel--top');
sentinels.forEach(el => observer.observe(el));
}
观察器配置了 threshold: [0]
,因此其回调会立即触发
事件。
此过程与底部标记 (.sticky_sentinel--bottom
) 类似。
创建第二个观察器,以在页脚穿过底部时触发
滚动容器的位置。observeFooters
函数会创建
哨兵节点并将其挂接到各部分。观察者会计算
标记与容器底部的交集,用于决定容器是否
进入或离开。该信息决定了章节标题
/**
* Sets up an intersection observer to notify when elements with the class
* `.sticky_sentinel--bottom` become visible/invisible at the bottom of the
* container.
* @param {!Element} container
*/
function observeFooters(container) {
const observer = new IntersectionObserver((records, observer) => {
for (const record of records) {
const targetInfo = record.boundingClientRect;
const stickyTarget = record.target.parentElement.querySelector('.sticky');
const rootBoundsInfo = record.rootBounds;
const ratio = record.intersectionRatio;
// Started sticking.
if (targetInfo.bottom > rootBoundsInfo.top && ratio === 1) {
fireEvent(true, stickyTarget);
}
// Stopped sticking.
if (targetInfo.top < rootBoundsInfo.top &&
targetInfo.bottom < rootBoundsInfo.bottom) {
fireEvent(false, stickyTarget);
}
}
}, {threshold: [1], root: container});
// Add the bottom sentinels to each section and attach an observer.
const sentinels = addSentinels(container, 'sticky_sentinel--bottom');
sentinels.forEach(el => observer.observe(el));
}
观察器配置了 threshold: [1]
,因此当
整个节点都在视图中
最后,还有两个实用程序可用于触发 sticky-change
自定义事件
并生成标记:
/**
* @param {!Element} container
* @param {string} className
*/
function addSentinels(container, className) {
return Array.from(container.querySelectorAll('.sticky')).map(el => {
const sentinel = document.createElement('div');
sentinel.classList.add('sticky_sentinel', className);
return el.parentElement.appendChild(sentinel);
});
}
/**
* Dispatches the `sticky-event` custom event on the target element.
* @param {boolean} stuck True if `target` is sticky.
* @param {!Element} target Element to fire the event on.
*/
function fireEvent(stuck, target) {
const e = new CustomEvent('sticky-change', {detail: {stuck, target}});
document.dispatchEvent(e);
}
大功告成!
最终演示
我们创建了一个自定义事件,当具有 position:sticky
的元素变为
修复了并在不使用 scroll
事件的情况下添加了滚动效果的问题。
总结
我经常想知道IntersectionObserver
是否会
有助于替换一些基于事件的 scroll
界面模式,
所取得的成就。事实证明,答案是“是”和“否”。语义
IntersectionObserver
API 的所有功能,这使得它很难实现所有目的。但作为
您可以将它用于一些有趣的技巧。
检测样式更改的另一种方法是什么?
不一定。我们需要的是一种观察 DOM 元素样式变化的方法。 遗憾的是,在网络平台 API 中,没有任何内容可以让您 手表样式更改。
MutationObserver
是合理的首选选择,但不适用于
。例如,在演示中,当 sticky
调用
类会添加到元素中,但不会在元素计算的样式发生变化时添加。
回想一下,sticky
类已在网页加载时声明。
将来,
“Style Mutation Observer”
Mutation Observer 的扩展程序可能有助于观察
元素经过计算的样式。
position: sticky
。