Intersection Observer 来到 Firefox

无限滚动、延迟加载和在线广告有什么共同点?

他们需要了解并对页面上元素的可见性做出反应!

不幸的是,传统上在 Web 上确定元素是否可见非常困难。大多数解决方案监听 scrollresize 事件,然后使用 DOM API,如 getBoundingClientRect() 手动计算元素相对于视窗的位置。这通常有效,但效率低下,并且没有考虑元素可见性发生变化的其他方式,例如页面上方的较大图像最终加载完毕,从而将所有其他内容向下推。

对于广告来说,情况变得更糟,因为涉及到真金白银。正如 Malte Ubl他的演讲 中所解释的那样,广告商不想为从未显示的广告付费。为了确保他们知道广告何时可见,他们用数十个微小的、单像素的 Flash 电影来覆盖广告,可以从它们的帧速率推断出它们的可见性。在没有 Flash 的平台上,例如智能手机,广告商会设置计时器来强制浏览器每隔几毫秒重新计算每个广告的位置。

这些技术会降低性能、消耗电池,并且如果浏览器可以通知我们元素的可见性何时发生变化,那么这些技术将是完全不必要的

这就是 IntersectionObserver 的作用。

您好,新的 IntersectionObserver()

在最基本的情况下,IntersectionObserver API 看起来像这样

let observer = new IntersectionObserver(handler);
observer.observe(target); // <-- Element to watch

下面的 演示 显示了一个简单的处理程序在实际中的作用。

单个观察者可以同时观察多个目标元素;只需对每个目标重复调用 observer.observe()

交叉?我以为这是关于可见性的?

默认情况下,IntersectionObservers 计算目标元素与页面可见部分(也称为浏览器的“视窗”)的重叠程度(或“与…相交”):

Illustration of a target element partially intersecting with a browser's viewport

但是,观察者还可以监控元素与任意父元素的重叠程度,而无论实际的屏幕可见性如何。这对于按需加载内容的小部件很有用,例如容器 div 内的无限滚动列表。在这种情况下,小部件可以使用 IntersectionObservers 来帮助加载足够的内容以填充其容器。

为简单起见,本文的其余部分将以“可见性”来讨论事物,但请记住,IntersectionObservers 不一定局限于字面上的可见性。

处理程序基础

观察者处理程序是回调函数,它们接收两个参数

  1. 一个 IntersectionObserverEntry 对象列表,每个对象都包含有关自上次调用处理程序以来目标交叉点如何变化的元数据。
  2. 对观察者本身的引用。

观察者默认监控浏览器的视窗,这意味着上面的演示只需要查看 isIntersecting 属性来确定目标元素的任何部分是否可见。

默认情况下,处理程序仅在目标元素从完全屏幕外过渡到部分可见或反之亦然时运行,但是如果您想区分部分可见和完全可见的元素怎么办?

阈值来救援!

使用阈值

除了处理程序回调之外,IntersectionObserver 构造函数还可以接受一个包含观察者几个配置选项的对象。其中一个选项是 threshold,它定义了调用处理程序的断点。

let observer = new IntersectionObserver(handler, {
    threshold: 0 // <-- This is the default
});

默认的 threshold0,只要目标变得部分可见或完全不可见就会调用处理程序。将 threshold 设置为 1 会在目标在完全可见和部分可见之间切换时触发处理程序,而将其设置为 0.5 会在目标经过 50% 可见性点时触发处理程序,无论方向如何。

您还可以提供一个阈值数组,如下面的 演示 中的 threshold: [0, 1] 所示

缓慢滚动目标进出视窗,并观察其行为。

目标从完全可见开始 - 它的 intersectionRatio1 - 并且在它从屏幕上滚动出去时发生两次变化:一次变为类似 0.87,然后变为 0。当目标滚动回到视图中时,它的 intersectionRatio 更改为 0.05,然后更改为 101 有道理,但额外的值来自哪里,以及 01 之间的其他所有数字呢?

阈值是根据过渡定义的:只要浏览器注意到目标的 intersectionRatio 已经超过或缩小到某个阈值,就会触发处理程序。将阈值设置为 [0, 1] 告诉浏览器“只要目标跨越无可见性 (0) 和完全可见性 (1) 的界限,就通知我”,这实际上定义了三种状态:完全可见、部分可见和不可见。

观察到的 intersectionRatio 值因测试而异,因为浏览器必须等待空闲时刻才能检查并报告交叉点;这些计算在后台发生,优先级低于滚动或用户输入等操作。

尝试 编辑 CodePen 以添加或删除阈值。观察当处理程序运行时和运行位置的变化。

其他选项

IntersectionObserver 构造函数可以接受另外两个选项

  • root:要观察的区域(默认值:浏览器视窗)。
  • rootMargin:在计算交叉点时缩小或扩展根节点的逻辑大小的程度(默认值:"0px 0px 0px 0px")。

更改 root 允许观察者检查相对于父容器元素的交叉点,而不仅仅是浏览器的视窗。

增加观察者的 rootMargin 可以检测目标何时接近给定区域。例如,观察者可以在即将变得可见之前等待加载屏幕外图像。

浏览器支持

IntersectionObserver 在 Edge 15、Chrome 51 和 Firefox 55 中默认可用,Firefox 55 预计在下周发布。

一个 polyfill 可用,它在所有地方都有效,尽管没有本地实现的性能优势。

其他资源

关于 Dan Callahan

Mozilla 开发者关系工程师,前 Mozilla Persona 开发人员。

更多 Dan Callahan 的文章…


8 条评论

  1. Simon

    假设我有一个可滚动的容器元素,它包含大量图像元素,例如……(想想一个图像库)。其中许多将由浏览器在屏幕外加载。FF 是否已经做了一些优化(只加载和绘制可见的图像),Intersection Observer 会干扰它吗?

    2017 年 8 月 3 日 上午 00:06

  2. Šime Vidas

    对于那些想在演示中尝试不同选项的人,请切换到“调试视图”。似乎 `rootMargin` 选项在 CodePen 的其他模式下不起作用。

    2017 年 8 月 3 日 上午 01:10

  3. Eric Shepherd

    大家好!我在 MDN 上写了这个 API 的文档,所以如果您在玩它时发现文档中存在不清楚或需要改进的地方,并且您不想自己更新文档(别忘了 - MDN 是一个维基!),请务必通过以下表格告知我们问题:https://bugzilla.mozilla.org/form.doc

    我非常喜欢编写这个 API 的文档。它做得非常好,可以以多种创造性的方式使用。尽情享用!

    2017 年 8 月 4 日 上午 07:12

  4. Eduardo

    内容很棒。我正在做一个项目,现在我决定使用这个功能重写代码。谢谢!

    2017 年 8 月 4 日 上午 07:35

  5. Oliver

    在回调处理程序中必须使用 for of 或 forEach 遍历条目吗?
    下面的代码有效(对于简单的情况),并且更简单。

    function observerCallback(entries, observer) {   if (entries[0].isIntersecting) {     document.body.style.backgroundColor = "blue"   } else {     document.body.style.backgroundColor = "red"   } }

    2017 年 8 月 9 日 下午 2:04

    1. Dan Callahan

      遍历条目数组很重要:根据浏览器正在执行的其他工作,即使在其他情况下,也可能有多个事件排队并同时传递给处理程序简单的情况。

      2017 年 8 月 9 日 下午 1:00

  6. Jeremy Wagner

    我正在将其实现到一个延迟加载库中。当我所有的元素都延迟加载时,我应该使用 unobserve 还是 disconnect,或者浏览器会自动处理移除观察者吗?

    2017 年 8 月 10 日 上午 11:22

    1. Dan Callahan

      是的,你应该在完成后手动移除观察者。

      正如你提到的,你有两个选择

      1. 使用 observer.disconnect() 完全关闭所有观察目标的观察者。
      2. 使用 observer.unobserve(target) 关闭特定目标的观察者。

      除了isIntersectingintersectionRatio 之类的属性之外,传递给你的处理程序的 IntersectionObserverEntry 还有一个包含目标引用的target 属性,你可以将其传递给unobserve()

      2017 年 8 月 11 日 上午 11:47

本文评论已关闭。