原文链接:https://developers.google.com/web/updates/2017/09/sticky-headers
太长不看版本:
这是个秘密:也许在你的下一个应用不再需要scroll事件. 使用 IntersectionObserver,我会给你展示,在不监听scroll事件的情况下,position:sticky元素变成fixed或者停止sticking时怎么触发自定义事件。这里有一个酷炫的demo去证明:
知乎视频www.zhihu.com查看示例 | 源码
sticky-change event 介绍
一个 CSS position:sticky 缺失的事件。
使用CSS 粘性布局的一个实际限制是没有提供一个信号去识别什么时候这个属性是激活的。换句话说,没有事件知道什么时候元素变成粘性附着或停止粘性附着。
请看下面的例子,固定距离顶部容器10像素的元素 <div class="sticky"> 。
.sticky { position: sticky; top: 10px;}
如果浏览器能告诉我元素啥时候到达该标记该有多好。显然不是我一个人这么想的。postion:sticky的事件可以解锁一系列的使用场景:
- 当横幅固定的时候添加个阴影。
- 用户阅读你的内容的时候,记录分析命中以记录他们的进度。
- 当用户滚动页面时,将浮动的TOC小部件更新为当前部分。
考虑到这些场景,我们制定了最终目标:创造一个当一个postion: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;
});
下面这个示例通过使用这个事件,给header元素在变为fixed的时候添加了个阴影。同时更改了页面顶部的标题。
知乎视频www.zhihu.com上面这个例子不使用滚动事件实现了这种效果。
没有滚动事件的滚动效果?
![8cf03e21be6123d90cdfc4d941f837f6.png](https://i-blog.csdnimg.cn/blog_migrate/33ef1cff2ebdf35f1199914396feace4.jpeg)
我们先声明一些术语,以便在本文的其余部分中都引用这些名称:
- 滚动区域 – 内容区域(可视视口)包含了一个文章的列表。
- 头部 – 每一部分的声明了position:sticky蓝色标题。
- 粘性部分 – 每一个内容部分。文本在粘性的头部下滚动。
- 粘性模式 – 当
position:sticky
应用到元素上。
为了知道哪个头部进入“粘性模式”,我们需要一些方法判定滚动区域的滚动偏移量。这样我们就知道如何计算当前头部处于哪种模式。然而如果不使用滚动事件的话,要做到这些是非常棘手的:) 另外一个问题是当元素变为固定的时候 position:sticky
把元素从布局中移除了。
所以没有滚动事件,我们失去了在头部上去计算布局相关的能力。
添加哨兵元素去检测滚动的位置
相比于使用滚动事件,我们将会使用IntersectionObserver
头检测合适头部进入和离开粘性模式 。在每个粘性部分添加两个节点(又名哨兵),一个添加在顶部,一个添加在底部,我们将其用作确定滚动位置的航路点,当哨兵进入和离开容器的时候,他们可视的能见性改变的时候,IntersectionObserver
触发一个回调。
![e6f5b82dbfcf6eee7cdce336c4a37c52.gif](https://i-blog.csdnimg.cn/blog_migrate/7fa52ccef219120ee72b3505e203b752.gif)
我们需要两个哨兵涵盖上下滚动的四种情况:
- 向下滚动 – 当头部哨兵穿过容器的顶部时,头部进入粘性模式。
- 向下滚动 – 当头部触达部分的底部而且底部哨兵穿过容器顶部时,头部离开粘性模式。
- 向上滚动 – 当顶部哨兵从上面滚回可见区域的时候,头部进入粘性模式。
- 向上滚动 – 当底部哨兵从上方回到可见区域的时候,头部进入粘性模式。
当哨兵进入离开滚动容器的时候Intersection Observers触发回调。
The CSS
哨兵元素被放置在每一部分的顶部和底部。.sticky_sentinel--top
被放在顶部,.sticky_sentinel--bottom
被放在底部。
![ffa7718e4a4fd982f7b173f4010f5b3d.png](https://i-blog.csdnimg.cn/blog_migrate/74774a8dd4ef8bb56ac9794ed5bb4aa2.jpeg)
: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 Observers
Intersection Obsserver 异步观察目标元素与文档视口或父容器相交处的变化。在本例中,我们观察是否与父容器相交。
每个哨兵都有一个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'));
然后,我添加了个IntersectionObserver,当.sticky_sentinel--top元素通过滚动容器的顶部时(任一方向)触发一个事件,observeHeaders
函数创建顶部的哨兵并将其添加到每个部分。 IntersectionObserver计算哨兵与容器顶部的相交点,并确定哨兵是否进入或离开视口。 通过这个来确定头部是否固定。
/**
* 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));
}
Observer配置了threshold: [0]
,因此一旦哨兵可见,它的回调就会触发。
底部哨兵的过程与此类似 (.sticky_sentinel--bottom
)。当页脚穿过滚动容器的底部时,创建的第二个Observer的回调将会触发 。observeFooters
函数创建哨兵节点,然后把他们绑定岛那个部分。这个Observer计算哨兵与容器底部的相交点,并确定它是否进入或离开。我们用这个信息确定头部是否处于粘性状态。
/**
* 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));
}
这个Observer配置了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的元素进入粘性模式,并且不使用滚动事件添加滚动效果。
知乎视频www.zhihu.com查看示例 | 源码
最终结论
我经常好奇IntersectionObserver
是否是取代基于滚动事件的UI模式的有利工具,答案是也不是。IntersectionObserver API用起来实现什么都比较困难。但是正向我刚刚展示的那样,你可以用它做一些有意思的事。
检测样式改变的另一种方式?
并不是的。 我们需要的是一种在DOM元素上观察样式更改的方法。 不幸的是,Web平台API中没有任何东西可以让您检测样式更改。
MutationObserver将是首选,但在大多数情况下不起作用。 例如,在演示中,当将粘性类添加到元素时,我们会收到回调,但是当元素的计算样式更改时,我们不会收到回调。 回想一下,粘性类已经在页面加载时已经声明了。
将来,Mutaion Observer的扩展"Style Mutation Observer" 也许会是对观察position: sticky
.的元素计算样式改变的有利工具。