Intersection Observer API 提供了一种异步监测目标元素与祖先元素或顶级文档视口相交情况变化的方式。
在历史上,检测元素的可见性,或者相对于彼此的两个元素的相对可见性,一直是一项困难的任务,解决方案不可靠,容易导致浏览器和用户访问的站点变得迟缓。
随着互联网的成熟,对这种类型的信息的需求也越来越大。相交信息在许多方面都是必要的,例如:
- 随着页面的滚动,对图像或其他内容进行延迟加载。
- 实现"无限滚动"网站,随着滚动加载和渲染更多内容,使用户无需翻页。
- 报告广告的可见性,以计算广告收入。
- 根据用户是否会看到结果来决定是否执行任务或动画处理。
过去实现交叉检测涉及事件处理程序和循环调用像 Element.getBoundingClientRect() 这样的方法,为每个受影响的元素收集所需信息。由于所有这些代码在主线程上运行,即使其中一个也可能导致性能问题。当网站加载了这些测试时,情况可能变得非常糟糕。
考虑一个使用无限滚动的网页。它使用供应商提供的库来管理定期放置在整个页面中的广告,在某些地方使用动画图形,并使用自定义库绘制通知框等。 每个库都有自己的交叉检测例程,全部在主线程上运行。网站的作者甚至可能没有意识到这是正在发生的事情,因为他们可能对他们正在使用的两个库的内部工作知之甚少。 随着用户滚动页面,这些交叉检测例程在滚动处理代码期间不断触发,导致用户对浏览器、网站和计算机感到沮丧。
Intersection Observer API 允许代码注册回调函数,每当要监视的元素进入或退出另一个元素(或视口)时,或者当两者的交集量按请求的量变化时,该函数将被执行。这样,网站不再需要在主线程上执行任何操作来监视这种元素交叉情况,浏览器可以根据需要优化交叉管理。
Intersection Observer API 无法告诉您的一件事:重叠的像素数或特定的重叠像素;然而,它涵盖了更常见的用例,即"如果它们交叉大约 N%,我需要做某事。"
交叉观察者的概念和用法
交叉观察者API允许您配置一个回调函数,当发生以下任一情况时调用该函数:
- 目标元素与设备的视口或指定的元素相交。所指定的元素在Intersection Observer API中被称为根元素或根。
- 观察者首次被要求监视目标元素。
通常情况下,您希望监视与目标元素最近的可滚动祖先的相交变化,或者如果目标元素不是可滚动元素的子元素,则是设备的视口。 要相对于设备的视口监视相交情况,请为根选项指定null。继续阅读以获取有关交叉观察者选项的更详细解释。
无论您是使用视口还是其他元素作为根,API 的工作方式都是相同的,它会在目标元素的可见性发生变化并且它与根的相交量达到所需值时执行您提供的回调函数。
目标元素与其根之间的相交程度称为相交比率。这是一个介于0.0和1.0之间的值,表示目标元素可见部分的百分比。
创建一个交叉观察者
通过调用其构造函数并传递一个回调函数来创建交叉观察者,该回调函数在某个方向上越过阈值时会被执行
js
let options = {
root: document.querySelector("#scrollArea"),
rootMargin: "0px",
threshold: 1.0,
};
let observer = new IntersectionObserver(callback, options);
交叉观察者选项
传递给 IntersectionObserver() 构造函数的选项对象允许您控制观察者的回调在何种情况下被调用。它具有以下字段:
root
用于检查目标可见性的视口元素。必须是目标元素的祖先。如果未指定或为 null,则默认为浏览器视口。
rootMargin
视口元素周围的边距。可以具有类似于 CSS 边距属性的值,例如 "10px 20px 30px 40px"(上、右、下、左)。值可以是百分比。这组值用于在计算交集之前扩大或缩小根元素边界框的每个边。默认为全零。
threshold
一个数字或数字数组,指示在目标可见性的百分之多少时,观察者的回调应该执行。如果您只想在可见性超过 50% 的标记时检测到,可以使用值 0.5。如果您希望在每次可见性超过另外 25% 时运行回调,可以指定数组 [0, 0.25, 0.5, 0.75, 1]。 默认值为 0(即只要有一个像素可见,就会运行回调)。值为 1.0 表示在每个像素都可见之前,不会认为已通过阈值。
选择要观察的目标元素
创建观察者后,您需要为它提供要观察的目标元素:
js
let target = document.querySelector("#listItem");
observer.observe(target);
// the callback we setup 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 对象的列表以及观察者本身:
js
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 选项,则目标必须是根元素的后代。
交叉如何计算
Intersection Observer API 考虑的所有区域都是矩形;形状不规则的元素被认为占据包围其所有部分的最小矩形。 同样地,如果元素的可见部分不是矩形,则元素的交叉矩形被认为是包含元素所有可见部分的最小矩形。
了解一些由 IntersectionObserverEntry 提供的属性如何描述交叉是很有用的。
根元素和根边距
在跟踪元素与容器的交叉之前,我们需要知道容器是什么。这个容器就是根元素。它可以是文档中的一个特定元素,它是要被观察的元素的祖先,或者是 null,用于使用文档的视口作为容器。
根交叉矩形是用于与目标或目标进行比较的矩形。这个矩形的确定方法如下:
- 如果根元素是隐式根(即顶层 Document),根交叉矩形是视口的矩形。
- 如果根元素具有溢出剪辑,根交叉矩形是根元素的内容区域。
- 否则,根交叉矩形是根元素的边界客户端矩形。
在创建 IntersectionObserver 时,通过设置 root margin(根边距) rootMargin,可以进一步调整根交叉矩形。rootMargin 中的值定义了添加到根元素边界框的每个边的偏移量,以创建最终的根元素边界。
阈值
Intersection Observer API 不会报告目标元素可见性的每一个微小变化,而是使用阈值。当您创建观察者时,可以提供一个或多个表示目标元素可见部分百分比的数值。然后,API 仅报告可见性的变化越过这些阈值的情况。
例如,如果您希望在每次目标的可见性通过每个 25% 的标记时被通知,您可以在创建观察者时将阈值列表指定为 [0, 0.25, 0.5, 0.75, 1]。
当回调函数被调用时,它会接收到一个 IntersectionObserverEntry 对象的列表,每个被观察目标都有一个交叉根交叉程度的度数,以使暴露的量在一个阈值上下交叉,无论是向前还是向后。
您可以通过查看 entry 的 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 以外的任何值都会导致剪切发生。
- 如果包含元素中的一个是嵌套浏览上下文的根(例如包含在 中的文档),则交叉矩形被剪切为包含上下文的视口,并通过容器的包含块继续向上递归。因此,如果达到 的顶层,交叉矩形将被剪切为帧的视口,然后帧的父元素是向交叉根递归的下一个块。
- 当向上递归到交叉根时,得到的矩形被映射到交叉根的坐标空间。
- 然后,通过将其与根交叉矩形相交,更新该矩形。
- 最后,该矩形被映射到目标文档的坐标空间中。
交叉变化回调函数
当目标元素的可见部分在根元素内穿过一个可见性阈值时,IntersectionObserver 对象的回调函数会被执行。回调函数的输入是一个 IntersectionObserverEntry 对象的数组,每个交叉的阈值都有一个对象,并且一个指向 IntersectionObserver 对象本身的引用。
在阈值列表中的每个条目都是描述已经交叉的一个阈值的 IntersectionObserverEntry 对象。每个条目描述了给定元素与根元素相交的程度,无论元素是否被认为相交,以及过渡发生的方向。
以下代码段显示了一个回调函数,它计算了从不与根元素相交到至少 75% 相交的元素过渡的次数。对于阈值值 0.0(默认值),回调函数在 isIntersecting 的布尔值发生转换时会被调用。因此,代码段首先检查过渡是否为正过渡,然后确定 intersectionRatio 是否超过 75%,在这种情况下,它会增加计数器。
js
const intersectionCallback = (entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
let elem = entry.target;
if (entry.intersectionRatio >= 0.75) {
intersectionCounter++;
}
}
});
};
Interfaces
IntersectionObserver
Intersection Observer API 的主要接口。提供了用于创建和管理观察者的方法,该观察者可以同时监视任意数量的目标元素,以进行相同的交叉配置。 每个观察者可以异步地观察一个或多个目标元素与共享的祖先元素或其顶级文档的视口之间的交叉变化。这个祖先元素或视口被称为根。
IntersectionObserverEntry
描述目标元素与其根容器在特定过渡时刻的交叉。此类型的对象只能通过两种方式获得:作为输入传递给您的 IntersectionObserver 回调,或通过调用 IntersectionObserver.takeRecords()。
一个简单的示例
这个简单的示例会在目标元素变得更加或更少可见时改变其颜色和透明度。在 "Timing element visibility with the Intersection Observer API" 中,您可以找到一个更详细的示例,展示了如何计算一组元素(如广告)对用户的可见时间,并通过记录统计信息或更新元素来响应该信息。
HTML
这个示例的 HTML 非常简短,主要包含一个元素,这个元素是我们的目标元素(id="box"),以及框内的一些内容。
js
<div id="box">
<div class="vertical">Welcome to <strong>The Box!</strong></div>
</div>
css
在这个示例中,CSS 并不是非常重要;它布局了元素,并且确定了 background-color 和 border 属性可以参与 CSS 过渡,我们将使用它们来影响元素在变得更加或更少被遮挡时的变化。
js
#box {
background-color: rgba(40, 40, 190, 1);
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 代码。
设置
首先,我们需要准备一些变量并安装观察者。
js
const numSteps = 20.0;
let boxElement;
let prevRatio = 0.0;
let increasingColor = "rgba(40, 40, 190, ratio)";
let decreasingColor = "rgba(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() 来开始监听加载事件;一旦页面加载完成,我们使用 querySelector() 获取具有 id = "box" 的元素的引用,然后调用接下来将创建的 createObserver() 方法来处理构建交叉观察者。
创建交叉观察者
createObserver() 方法在页面加载完成后被调用,用于实际创建新的 IntersectionObserver 并开始观察目标元素的过程。
js
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(),以及我们的 options 集合。然后我们在返回的观察者上调用 observe(),将目标元素传递给它。
如果需要,我们可以选择通过为每个目标元素调用 observer.observe() 来监视多个元素与视口之间的可见性交叉变化。
构建阈值比率数组
构建阈值列表的 buildThresholdList() 函数如下所示:
js
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 之间的比率,通过将值 i/numSteps 推送到阈值数组中,其中 i 为介于 1 和 numSteps 之间的整数。它还将 0 推送到数组中以包括该值。给定 numSteps 的默认值(20),得到以下阈值列表:
# | Ratio | # | Ratio |
---|---|---|---|
1 | 0.05 | 11 | 0.55 |
2 | 0.1 | 12 | 0.6 |
3 | 0.15 | 13 | 0.65 |
4 | 0.2 | 14 | 0.7 |
5 | 0.25 | 15 | 0.75 |
6 | 0.3 | 16 | 0.8 |
7 | 0.35 | 17 | 0.85 |
8 | 0.4 | 18 | 0.9 |
9 | 0.45 | 19 | 0.95 |
10 | 0.5 | 20 | 1.0 |
当然,我们可以将阈值数组硬编码到我们的代码中,而且通常也会这样做。但是这个示例留出了添加配置控件以调整粒度的空间,例如。
处理交叉变化
当浏览器检测到目标元素(在我们的示例中是具有 id="box" 的元素)的可见性比率穿越了我们列表中的某个阈值,它会调用我们的处理函数 handleIntersect():
js
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;
});
}
对于列表中的每个 IntersectionObserverEntry,我们会查看条目的 intersectionRatio 是否在增加;如果是,我们会将目标的 background-color 设置为 increasingColor 中的字符串(记住,它是 "rgba(40, 40, 190, ratio)"),并将其中的 "ratio" 替换为条目的 intersectionRatio。结果:颜色不仅会改变,目标元素的透明度也会发生变化;随着交叉比率的下降,背景颜色的 alpha 值也会随之下降,从而得到一个更透明的元素。
同样地,如果 intersectionRatio 在下降,我们会使用字符串 decreasingColor,并在其中替换交叉比率前的 "ratio",然后设置目标元素的 background-color。
最后,为了跟踪交叉比率是在增加还是减少,我们将当前比率存储在变量 prevRatio 中。
结果
以下是生成的内容。上下滚动此页面,注意在这个过程中框的外观如何发生变化。 示例
在 "使用 Intersection Observer API 计时元素可见性" 中有一个更详细的示例。
规范
[Intersection Observer