交叉观察器 API
交叉观察器 API 提供了一种异步观察目标元素与祖先元素或顶级文档的视窗交叉部分变化的方法。
历史上,检测元素的可见性或两个元素之间相对可见性一直是一项艰巨的任务,其解决方案不可靠且容易导致浏览器和用户访问的网站变得迟缓。随着 Web 的发展,对这种信息的需要越来越大。交叉信息在许多方面都是必需的,例如
- 当页面滚动时懒加载图像或其他内容。
- 实现“无限滚动”网站,当您滚动时会加载和渲染越来越多的内容,这样用户就不必翻页。
- 报告广告的可见性,以便计算广告收入。
- 根据用户是否会看到结果来决定是否执行任务或动画过程。
过去,实现交叉检测涉及事件处理程序和循环调用诸如Element.getBoundingClientRect()
之类的函数来构建每个受影响元素所需的信息。由于所有这些代码都在主线程上运行,即使其中一个也可能导致性能问题。当网站加载了这些测试时,情况可能会变得非常糟糕。
考虑一个使用无限滚动的网页。它使用供应商提供的库来管理定期放置在页面上的广告,这里和那里有动画图形,并使用自定义库来绘制通知框等。每个库都有自己的交叉检测例程,所有这些例程都在主线程上运行。网站作者可能甚至没有意识到这种情况正在发生,因为他们可能对他们使用的两个库的内部工作原理知之甚少。当用户滚动页面时,这些交叉检测例程在滚动处理代码期间不断触发,导致用户对浏览器、网站和他们的计算机感到沮丧。
交叉观察器 API 允许代码注册一个回调函数,该函数在特定元素进入或退出与另一个元素(或视窗)的交叉部分,或者当两个元素之间的交叉部分发生指定数量的变化时执行。这样,网站不再需要在主线程上执行任何操作来监视这种元素交叉,浏览器可以自由地根据需要优化交叉部分的管理。
交叉观察器 API 无法告诉您的一个信息是:重叠的像素的确切数量或具体是哪些像素;但是,它涵盖了更常见的用例“如果它们以大约N%重叠,我需要做一些事情”。
交叉观察器概念和用法
交叉观察器 API 允许您配置一个回调函数,该函数在以下两种情况之一发生时调用
- 目标元素与设备的视窗或指定元素交叉。该指定元素称为交叉观察器 API 的根元素或根。
- 首次要求观察者观察目标元素时。
通常,您需要监视目标元素与最接近的可滚动祖先的交叉部分变化,或者,如果目标元素不是可滚动元素的后代,则监视目标元素与设备视窗的交叉部分变化。要监视相对于设备视窗的交叉部分,请为root
选项指定null
。继续阅读以了解有关交叉观察器选项的更详细说明。
无论您使用视窗还是其他元素作为根,API 的工作方式都相同,都会执行您提供的回调函数,无论何时目标元素的可见性发生变化,以便它与根交叉所需的交叉数量。
目标元素与其根之间的交叉程度称为交叉比率。它表示目标元素可见部分的百分比,是一个介于 0.0 和 1.0 之间的值。
创建交叉观察者
通过调用其构造函数并向其传递一个回调函数来创建交叉观察者,该函数在某个阈值在一个方向或另一个方向被越过时运行
let options = {
root: document.querySelector("#scrollArea"),
rootMargin: "0px",
threshold: 1.0,
};
let observer = new IntersectionObserver(callback, options);
阈值 1.0 表示,当目标的 100% 在由root
选项指定的元素内可见时,回调函数被调用。
交叉观察器选项
传递给IntersectionObserver()
构造函数的options
对象允许您控制观察者的回调函数被调用的情况。它具有以下字段
root
-
用作视窗来检查目标可见性的元素。必须是目标的祖先。如果未指定或为
null
,则默认为浏览器视窗。 rootMargin
-
根周围的边距。可以具有类似于 CSS
margin
属性的值,例如"10px 20px 30px 40px"
(上、右、下、左)。这些值可以是百分比。这组值用于在计算交叉部分之前扩展或缩小根元素边界框的每一侧。默认值为全部为零。 threshold
-
一个数字或一组数字,表示观察者的回调函数应该在目标可见性的多少百分比时执行。如果您只想检测可见性何时超过 50% 的标记,您可以使用值 0.5。如果您希望回调函数在每次可见性超过 25% 时运行,则应指定数组 [0, 0.25, 0.5, 0.75, 1]。默认值为 0(表示只要可见一个像素,回调函数就会运行)。值 1.0 表示直到每个像素都可见才认为阈值已超过。
定位要观察的元素
创建观察者后,您需要为其提供要观察的目标元素
let target = document.querySelector("#listItem");
observer.observe(target);
// the callback we set up for the observer will be executed now for the first time
// it waits until we assign a target to our observer (even if the target is currently not visible)
每当目标满足为IntersectionObserver
指定的阈值时,就会调用回调函数。回调函数接收一个IntersectionObserverEntry
对象列表和观察者
let callback = (entries, observer) => {
entries.forEach((entry) => {
// Each entry describes an intersection change for one observed
// target element:
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
};
回调函数接收的条目列表包含每个报告其交叉状态发生变化的目标的一个条目。检查isIntersecting
属性的值,以查看条目是否表示当前与根交叉的元素。
请注意,您的回调函数是在主线程上执行的。它应该尽可能快地运行;如果需要执行任何耗时的操作,请使用Window.requestIdleCallback()
。
此外,请注意,如果您指定了root
选项,则目标必须是根元素的后代。
交叉部分的计算方式
交叉观察器 API 考虑的所有区域都是矩形;形状不规则的元素被认为占据了包含所有元素部分的最小矩形。类似地,如果元素的可见部分不是矩形,则元素的交叉矩形被认为是包含元素所有可见部分的最小矩形。
了解IntersectionObserverEntry
提供的各种属性如何描述交叉部分会很有用。
交叉根和根边距
在我们能够跟踪元素与容器的交叉部分之前,我们需要知道该容器是什么。该容器是交叉根或根元素。它可以是文档中特定元素,该元素是待观察元素的祖先,也可以是null
,以使用文档的视窗作为容器。
根交叉矩形是用于检查目标或目标的矩形。该矩形通过以下方式确定
- 如果交叉根是隐式根(即顶级
Document
),则根交叉矩形是视窗的矩形。 - 如果交叉根具有溢出剪辑,则根交叉矩形是根元素的内容区域。
- 否则,根交叉矩形是根交叉部分的边界客户端矩形(如调用
getBoundingClientRect()
时返回)。
通过在创建IntersectionObserver
时设置根边距rootMargin
,可以进一步调整根交叉矩形。rootMargin
中的值定义添加到交叉根边界框每一侧的偏移量,以创建最终的交叉根边界(在回调函数执行时,这些边界将在IntersectionObserverEntry.rootBounds
中公开)。正值扩展框,负值缩小框。
在下面的示例中,我们有一个可滚动框和一个最初不可见的元素。你可以调整根元素的右外边距,然后你会发现
- 如果外边距为负数,那么即使红色元素开始变得可见,它仍然不被视为与根元素相交,因为根元素的边界框被缩小了。
- 如果外边距为正数,那么即使红色元素不可见,它也会被视为与根元素相交,因为它与根元素的外边距区域相交。
阈值
Intersection Observer API 不报告目标元素可见性的每一个微小变化,而是使用 **阈值**。创建观察者时,你可以提供一个或多个数字值,表示目标元素可见的百分比。然后,API 只报告跨越这些阈值的可见性变化。
例如,如果你想在每次目标的可见性向前或向后穿过每个 25% 标记时得到通知,那么在创建观察者时,你将指定数组 [0, 0.25, 0.5, 0.75, 1] 作为阈值列表。
当回调被调用时,它会收到一个 IntersectionObserverEntry
对象列表,每个对象代表一个被观察的目标,其与根元素的相交程度发生了变化,使得暴露的量在任一方向上跨越了其中一个阈值。
你可以通过查看条目的 isIntersecting
属性来查看目标是否 **当前** 与根元素相交;如果它的值为 true
,则目标至少部分地与根元素或文档相交。这让你可以确定条目是否代表从元素相交到不再相交的转换,还是从不相交到相交的转换。
注意,可能存在零相交矩形,这可能发生在相交正好位于两者之间的边界上或 boundingClientRect
的面积为零时。目标和根元素共享边界线的状态不被视为足以被视为转换为相交状态。
为了感受阈值的工作原理,尝试滚动下面的框。它里面的每个彩色框都会显示它自身在所有四个角上可见的百分比,这样你就可以看到这些比率随着你滚动容器而随时间变化。每个框都有不同的阈值集。
- 第一个框对可见性的每个百分比都有一个阈值;也就是说,
IntersectionObserver.thresholds
数组是[0.00, 0.01, 0.02, /*…,*/ 0.99, 1.00]
。 - 第二个框只有一个阈值,在 50% 标记处。
- 第三个框的阈值每隔 10% 的可见性(0%,10%,20% 等)。
- 最后一个框的阈值每隔 25%。
裁剪和相交矩形
浏览器计算最终相交矩形的方式如下;所有这些都是为你完成的,但了解这些步骤有助于更好地理解相交何时会发生。
- 目标元素的边界矩形(即完全包围构成元素的每个组件的边界框的最小矩形)可以通过对目标调用
getBoundingClientRect()
来获得。这是相交矩形可能的最大尺寸。剩余步骤将删除任何不与根元素相交的部分。 - 从目标元素的直接父块开始,向外移动,每个包含块的裁剪(如果有)都会被应用到相交矩形。块的裁剪是根据两个块的交集以及由
overflow
属性指定的裁剪模式(如果有)确定的。将overflow
设置为除visible
之外的任何值都会导致裁剪发生。 - 如果包含元素之一是嵌套浏览上下文的根(例如,包含在
<iframe>
中的文档),则相交矩形会被裁剪到包含上下文的视窗,并且向上的递归会继续使用容器的包含块。因此,如果到达<iframe>
的顶层,则相交矩形会被裁剪到框架的视窗,然后框架的父元素是下一个递归遍历的块,直到到达相交根元素。 - 当向上的递归到达相交根元素时,得到的矩形将被映射到相交根元素的坐标空间。
- 然后,通过将得到的矩形与 根相交矩形 相交来更新它。
- 最后,这个矩形被映射到目标元素的
document
的坐标空间。
相交变化回调
当目标元素在根元素中可见的量跨越其中一个可见性阈值时,IntersectionObserver
对象的回调将被执行。回调接收作为输入的 IntersectionObserverEntry
对象数组,每个对象代表一个被跨越的阈值;也就是说,每个条目都描述了给定元素与根元素相交的程度,以及元素是否被视为相交,以及转换发生的 方向。
阈值列表中的每个条目都是一个 IntersectionObserverEntry
对象,描述一个被跨越的阈值;也就是说,每个条目都描述了给定元素与根元素相交的程度,以及元素是否被视为相交,以及转换发生的 方向。
下面的代码片段显示了一个回调,它记录了元素从不与根元素相交到至少与根元素相交 75% 的次数。对于 0.0(默认值)的阈值,回调将 大约 在 isIntersecting
的布尔值转换时被调用。因此,代码片段首先检查转换是否为正转换,然后确定 intersectionRatio
是否高于 75%,如果是,则增加计数器。
const intersectionCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let elem = entry.target;
if (entry.intersectionRatio >= 0.75) {
intersectionCounter++;
}
}
});
};
接口
IntersectionObserver
-
Intersection Observer API 的主要接口。提供用于创建和管理观察者的方法,该观察者可以监视任意数量的目标元素以获得相同的相交配置。每个观察者可以异步观察一个或多个目标元素与其共享的祖先元素或其顶层
Document
的 视窗 之间的相交变化。祖先或视窗被称为 **根元素**。 IntersectionObserverEntry
-
描述目标元素与其根容器在特定转换时刻的相交情况。此类型的对象只能通过两种方式获得:作为
IntersectionObserver
回调的输入,或通过调用IntersectionObserver.takeRecords()
。
一个简单的例子
这个简单的例子使目标元素在变得更可见或更不可见时改变其颜色和透明度。在 使用 Intersection Observer API 定时元素可见性 中,你可以找到一个更全面的例子,展示如何定时一组元素(例如广告)对用户的可见时长,并通过记录统计信息或更新元素来对这些信息做出反应。
HTML
此示例的 HTML 代码非常短,包含一个主元素,它是我们将要定位的框(具有创意 ID "box"
),以及框内的一些内容。
<div id="box">
<div class="vertical">Welcome to <strong>The Box!</strong></div>
</div>
CSS
CSS 对此示例而言并不重要;它布局了元素并确定 background-color
和 border
属性可以参与 CSS 过渡,我们将使用它来影响元素在变得更模糊或更清晰时的变化。
#box {
background-color: rgb(40 40 190 / 100%);
border: 4px solid rgb(20 20 120);
transition:
background-color 1s,
border 1s;
width: 350px;
height: 350px;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.vertical {
color: white;
font: 32px "Arial";
}
.extra {
width: 350px;
height: 350px;
margin-top: 10px;
border: 4px solid rgb(20 20 120);
text-align: center;
padding: 20px;
}
JavaScript
最后,让我们看看使用 Intersection Observer API 使事情发生的 JavaScript 代码。
设置
首先,我们需要准备一些变量并安装观察者。
const numSteps = 20.0;
let boxElement;
let prevRatio = 0.0;
let increasingColor = "rgb(40 40 190 / ratio)";
let decreasingColor = "rgb(190 40 40 / ratio)";
// Set things up
window.addEventListener(
"load",
(event) => {
boxElement = document.querySelector("#box");
createObserver();
},
false,
);
我们在这里设置的常量和变量是
numSteps
-
一个常量,指示我们想要在 0.0 和 1.0 的可见性比率之间有多少个阈值。
prevRatio
-
这个变量将用于记录上一次跨越阈值时的可见性比率;这将让我们弄清楚目标元素是变得更可见还是更不可见。
increasingColor
-
一个字符串,定义了当可见性比率增加时我们将应用于目标元素的颜色。这个字符串中的 "ratio" 将被替换为目标的当前可见性比率,这样元素不仅会改变颜色,而且还会随着它变得不那么模糊而变得越来越不透明。
decreasingColor
-
类似地,这是一个字符串,定义了当可见性比率减少时我们将应用的颜色。
我们调用 Window.addEventListener()
来开始监听 load
事件;一旦页面加载完成,我们就使用 querySelector()
获取具有 ID "box"
的元素的引用,然后调用我们将在稍后创建的 createObserver()
方法来处理构建和安装相交观察者。
创建相交观察者
createObserver()
方法在页面加载完成后被调用,用于处理实际创建新的 IntersectionObserver
并开始观察目标元素的过程。
function createObserver() {
let observer;
let options = {
root: null,
rootMargin: "0px",
threshold: buildThresholdList(),
};
observer = new IntersectionObserver(handleIntersect, options);
observer.observe(boxElement);
}
这首先设置一个 options
对象,其中包含观察者的设置。我们想要监视目标元素相对于文档视窗的可见性变化,因此 root
为 null
。我们不需要外边距,因此外边距偏移量 rootMargin
被指定为 "0px"。这会导致观察者监视目标元素边界与视窗边界之间的相交变化,没有任何添加(或减去)的空间。
可见性比率阈值列表 threshold
由函数 buildThresholdList()
构建。阈值列表是在此示例中以编程方式构建的,因为它们有很多,并且数量应该可以调整。
一旦 options
准备就绪,我们就创建新的观察者,调用 IntersectionObserver()
构造函数,指定一个在相交跨越其中一个阈值时调用的函数 handleIntersect()
,以及我们的选项集。然后,我们对返回的观察者调用 observe()
,并将所需的 目标元素传递给它。
如果我们想要这样做,我们可以选择监视多个元素相对于视窗的可见性相交变化,方法是对每个元素调用 observer.observe()
。
构建阈值比率数组
构建阈值列表的 buildThresholdList()
函数看起来像这样
function buildThresholdList() {
let thresholds = [];
let numSteps = 20;
for (let i = 1.0; i <= numSteps; i++) {
let ratio = i / numSteps;
thresholds.push(ratio);
}
thresholds.push(0);
return thresholds;
}
这构建了阈值数组——每个阈值都是 0.0 到 1.0 之间的比率,方法是对每个 1 到 numSteps
之间的整数 i
将值 i/numSteps
推入 thresholds
数组。它还将 0 推入以包含该值。结果,给定 numSteps
的默认值(20),是以下阈值列表
# | 比率 | # | 比率 |
---|---|---|---|
0 | 0.05 | 11 | 0.6 |
1 | 0.1 | 12 | 0.65 |
2 | 0.15 | 13 | 0.7 |
3 | 0.2 | 14 | 0.75 |
4 | 0.25 | 15 | 0.8 |
5 | 0.3 | 16 | 0.85 |
6 | 0.35 | 17 | 0.9 |
7 | 0.4 | 18 | 0.95 |
8 | 0.45 | 19 | 1 |
9 | 0.5 | 20 | 0 |
10 | 0.55 |
当然,我们可以将阈值数组硬编码到我们的代码中,而且通常你会这样做。但此示例为添加配置控件以调整粒度留下了空间,例如。
处理相交变化
当浏览器检测到目标元素(在本例中,是具有 ID "box"
的元素)已被揭示或遮挡,以至于其可见性比率超过了我们列表中的某个阈值时,它就会调用我们的处理函数 handleIntersect()
。
function handleIntersect(entries, observer) {
entries.forEach((entry) => {
if (entry.intersectionRatio > prevRatio) {
entry.target.style.backgroundColor = increasingColor.replace(
"ratio",
entry.intersectionRatio,
);
} else {
entry.target.style.backgroundColor = decreasingColor.replace(
"ratio",
entry.intersectionRatio,
);
}
prevRatio = entry.intersectionRatio;
});
}
对于列表 entries
中的每个 IntersectionObserverEntry
,我们都会查看该条目的 intersectionRatio
是否正在上升;如果正在上升,我们就会将目标的 background-color
设置为 increasingColor
中的字符串(请记住,它是 "rgb(40 40 190 / ratio)"
),并将“ratio”一词替换为条目的 intersectionRatio
。结果:不仅颜色会发生改变,目标元素的透明度也会改变;随着交叉比率的下降,背景颜色的 alpha 值也会随之下降,从而导致元素变得更加透明。
类似地,如果 intersectionRatio
正在下降,我们使用字符串 decreasingColor
,并在其中将“ratio”一词替换为 intersectionRatio
,然后设置目标元素的 background-color
。
最后,为了跟踪交叉比率是上升还是下降,我们在变量 prevRatio
中记录当前比率。
结果
规范
规范 |
---|
Intersection Observer # intersection-observer-interface |
浏览器兼容性
BCD 表格仅在浏览器中加载