Intersection Observer API - 用于异步观察目标元素与祖先元素或与顶级文档视口的交集的变化

Intersection Observer API(交集观察者 API)提供了一种方法来异步观察目标元素与祖先元素或与顶级文档视口的交集的变化。

从历史上看,检测一个元素的可见度或两个元素相互之间的相对可见度一直是一项艰巨的任务,解决方案不可靠并且容易导致浏览器和用户访问的站点变得缓慢。随着网络的成熟,对此类信息的需求也在增长。需要交集信息的原因有很多,例如:

  • 在页面滚动时延迟加载图像或其他内容。
  • 实现 “无限滚动” 网站,在您滚动时加载和呈现越来越多的内容,这样用户就不必翻阅页面。
  • 报告广告的可见度以计算广告收入。
  • 根据用户是否会看到结果来决定是否执行任务或动画处理。

在过去,实现交集检测涉及到事件处理程序和循环,调用 Element.getBoundingClientRect() 等方法来建立每个受影响元素的所需信息。由于所有这些代码都在主线程上运行,即使其中一个也会导致性能问题。当一个网站加载这些测试时,整个操作会变得非常丑陋。

考虑一个使用无限滚动的网页。它使用供应商提供的库来管理在整个页面中定期放置的广告,到处都有动画图形,并使用绘制通知框等的自定义库。它们每个都有自己的交集检测程序,都在主线程上运行。网站的作者甚至可能没有意识到这种情况的发生,因为他们可能对他们正在使用的两个库的内部工作原理知之甚少。当用户滚动页面时,这些交集检测程序在滚动处理代码期间不断触发,导致用户对浏览器、网站和他们的计算机的操作体验变差。

Intersection Observer API 允许代码注册一个回调函数,当他们希望监控的元素进入或退出另一个元素(或 viewport),或者当两者相交的量发生所要求的变化时,该函数就会被执行。这样,网站就不再需要在主线程上做任何事情来监视这种元素交集,而浏览器可以根据自己的需要自由地优化交集的管理。

Intersection Observer API 无法告诉您的一件事:重叠像素的确切数量或具体是哪些像素;然而,它涵盖了更常见的用例 “如果它们在 N% 左右的某个地方相交,我需要做一些事情”。

交集观察者的概念和用法

Intersection Observer API 允许您配置在发生以下任一情况时调用的回调:

  • 一个目标元素与设备的视口或指定的元素相交。这个指定的元素被称为根元素,用于交集观察者API的目的。
  • 第一次要求观察者观察目标元素时。

通常,您需要观察与元素最近的可滚动祖先相关的交集变化,或者,如果该元素不是可滚动元素的后代,则是视口。要观察相对于根元素的交集,请指定 null

无论你是使用视口还是其他元素作为根,API的工作方式都是一样的,每当目标元素的可见度发生变化,使其与根的交集达到所需数量时,就会执行你提供的回调函数。

无论您是使用视口还是其他元素作为根,API 都以相同的方式工作,每当目标元素的可见度发生变化以使其与根相交所需的交集量时,就会执行您提供的回调函数。

目标元素与其根的交集度称为交集比。它以百分比表示目标元素的可见度,它是一个在 0.01.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);

// 我们为观察者设置的回调,会等到我们为观察者分配一个目标(即使目标当前不可见)就开始首次运行。

每当目标满足为 IntersectionObserver 指定的阈值时,就会调用回调。回调的参数为 IntersectionObserverEntry 对象和观察者的列表:

let callback = (entries, observer) => {
  entries.forEach(entry => {
    // 每个条目描述了一个观察到的交集变化
    // 目标元素:
    //   entry.boundingClientRect
    //   entry.intersectionRatio
    //   entry.intersectionRect
    //   entry.isIntersecting
    //   entry.rootBounds
    //   entry.target
    //   entry.time
  });
};

回调接收到的条目列表包括每个目标的一个条目,这些条目报告了其交集状态的变化。检查 isIntersecting 属性的值以查看条目是否表示当前与根相交的元素。

请注意,您的回调是在主线程上执行的。它应该尽快运行;如果需要做任何耗时的事情,请使用 Window.requestIdleCallback()

另请注意,如果您指定了 root 选项,则目标必须是根元素的后代。

如何计算交集

Intersection Observer 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% 的阈值。
<style>
main {
  position: relative;
  height: 500px;
  overflow: scroll;
}

.contents {
  position: absolute;
  width: 700px;
  height: 1725px;
}

.wrapper {
  position: relative;
  top: 600px;
}

.sampleBox {
  position: relative;
  left: 175px;
  width: 150px;
  background-color: rgb(245, 170, 140);
  border: 2px solid rgb(201, 126, 17);
  padding: 4px;
  margin-bottom: 6px;
}

#box1 {
  height: 200px;
}

#box2 {
  height: 75px;
}

#box3 {
  height: 150px;
}

#box4 {
  height: 100px;
}

.label {
  font: 14px "Open Sans", "Arial", sans-serif;
  position: absolute;
  margin: 0;
  background-color: rgba(255, 255, 255, 0.7);
  border: 1px solid rgba(0, 0, 0, 0.7);
  width: 3em;
  height: 18px;
  padding: 2px;
  text-align: center;
}

.topLeft {
  left: 2px;
  top: 2px;
}

.topRight {
  right: 2px;
  top: 2px;
}

.bottomLeft {
  bottom: 2px;
  left: 2px;
}

.bottomRight {
  bottom: 2px;
  right: 2px;
}
</style>

<template id="boxTemplate">
  <div class="sampleBox">
    <div class="label topLeft"></div>
    <div class="label topRight"></div>
    <div class="label bottomLeft"></div>
    <div class="label bottomRight"></div>
  </div>
</template>

<main>
  <div class="contents">
    <div class="wrapper">
    </div>
  </div>
</main>

<script>
let observers = [];

startup = () => {
  let wrapper = document.querySelector(".wrapper");

  // 观察者的选项
  let observerOptions = {
    root: null,
    rootMargin: "0px",
    threshold: []
  };

  // 每个盒子的一组阈值集。
  // 第一个盒子的阈值以编程方式设置,因为会有很多(每个百分点都有)。
  let thresholdSets = [
    [],
    [0.5],
    [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
    [0, 0.25, 0.5, 0.75, 1.0]
  ];

  for (let i=0; i<=1.0; i+= 0.01) {
    thresholdSets[0].push(i);
  }

  // 添加每个盒子,为每个盒子创建一个新的观察者
  for (let i=0; i<4; i++) {
    let template = document.querySelector("#boxTemplate").content.cloneNode(true);
    let boxID = "box" + (i+1);
    template.querySelector(".sampleBox").id = boxID;
    wrapper.appendChild(document.importNode(template, true));

    // 为这个盒子设置观察者
    observerOptions.threshold = thresholdSets[i];
    observers[i] = new IntersectionObserver(intersectionCallback, observerOptions);
    observers[i].observe(document.querySelector("#" + boxID));
  }

  // 滚动到起始位置
  document.scrollingElement.scrollTop = wrapper.firstElementChild.getBoundingClientRect().top + window.scrollY;
  document.scrollingElement.scrollLeft = 750;
}

intersectionCallback = (entries) => {
  entries.forEach((entry) => {
    let box = entry.target;
    let visiblePct = (Math.floor(entry.intersectionRatio * 100)) + "%";

    box.querySelector(".topLeft").innerHTML = visiblePct;
    box.querySelector(".topRight").innerHTML = visiblePct;
    box.querySelector(".bottomLeft").innerHTML = visiblePct;
    box.querySelector(".bottomRight").innerHTML = visiblePct;
  });
}

startup();
</script>

尝试一下 »

裁剪和交集矩形

浏览器按如下方式计算最终的交集矩形;这一切都已为您完成,但了解这些步骤有助于更好地准确掌握何时会发生交集。

  1. 通过调用目标元素的 getBoundingClientRect(),获得目标元素的边界矩形(即完全包围构成该元素的每个组件的边界框的最小矩形)。这是最大的交集矩形。剩下的步骤将删除任何不相交的部分。这可能是最大的交集矩形。剩余的步骤将删除任何不相交的部分。

  2. 从目标的直接父块开始,向外移动,每个包含的块的片段(如果有的话)被应用到交集矩形。块的剪裁是根据两个块的交集和 overflow 属性指定的剪裁模式(如果有)确定的。将 overflow 设置为除 visible 之外的任何内容会导致发生剪切。

  3. 如果包含元素之一是嵌套浏览上下文的根(例如包含在 <iframe> 中的文档,则交集矩形将被剪切到包含上下文的视口,并向上递归通过容器继续容器的包含块。所以如果到达顶层的 <iframe>,交集矩形被裁剪到框架的视口,然后框架的父元素是下一个向交集根递归通过的块。

  4. 当向上递归到达交集根时,生成的矩形被映射到交集根的坐标空间。

  5. 然后通过将其与根交集矩形 相交来更新生成的矩形。

  6. 最后,这个矩形被映射到目标的 document 的坐标空间。

交集变化回调

当根元素中可见的目标元素的数量超过其中一个可见度阈值时,IntersectionObserver 对象的回调被执行。该回调接收所有 IntersectionObserverEntry 对象的数组作为输入,每个被越过的阈值对应一个对象,还有一个对 IntersectionObserver 对象本身的引用。

阈值列表中的每个条目都是一个 IntersectionObserverEntry 对象,描述了一个被跨越的阈值;也就是说,每个条目描述了给定元素与根元素相交的程度,元素是否被认为是相交的,以及发生转换的方向。

下面的代码片段显示了一个回调,它保留了一个计数器,记录元素从不与根相交到至少相交 75% 的次数。对于阈值为 0.0(默认)的回调,大约isIntersecting 的布尔值转换时被调用。因此,该片段首先检查过渡是否为正值,然后确定 intersectionRatio 是否高于75%,在这种情况下,它会增加计数器。

intersectionCallback(entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      let elem = entry.target;

      if (entry.intersectionRatio >= 0.75) {
        intersectionCounter++;
      }
    }
  });
}

接口

IntersectionObserver

Intersection Observer API 的主要接口。提供创建和管理观察者的方法,观察者可以观察相同交集配置的任意数量的目标元素。每个观察者都可以异步观察一个或多个目标元素与共享祖先元素之间的交集或与其顶级 Documentviewport 的变化。祖先或视口称为

IntersectionObserverEntry

描述在特定转换时刻目标元素与其根容器之间的交集。这种类型的对象只能通过两种方式获得:作为 IntersectionObserver 回调的输入,或者通过调用 IntersectionObserver.takeRecords() 获得。

一个简单的例子

这个简单的例子使目标元素在可见度变化时,改变它的颜色和透明度。在使用 Intersection Observer API 计时元素可见度中,你可以找到一个更广泛的例子,该示例展示了如何计时一组元素(如广告)对用户可见的时间,并通过记录统计数据或更新元素来对该信息做出反应。

HTML

此实例的 HTML 非常简短,主要元素是我们将定位的盒子(带有广告素材 ID "box")和盒子内的一些内容。

<div id="box">
  <div class="vertical">
    欢迎来到<strong>盒子!</strong>
  </div>
</div>

CSS

就本实例而言,CSS 并不是非常重要;它布置元素并设置了 background-colorborder 属性可以参与 CSS 转换,我们将用它来影响该元素的变化,因为它或多或少会被遮挡。

#box {
  background-color: rgba(40, 40, 190, 255);
  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 = "rgba(40, 40, 190, ratio)";
let decreasingColor = "rgba(190, 40, 40, ratio)";

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 对象。我们想观察目标元素是相对于文档视口的可见度变化,因此 rootnull。我们不需要边距,因此边距偏移量 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;
}

循环 1 到 numSteps 之间的每个整数 i,将数值 i/numSteps 推到 thresholds 数组中,这样就建立了阈值数组(每个阈值都是 0.0 和 1.0 之间的比率)。它还将 0 加入到阈值数组中。给定 numSteps (20) 的默认值,结果是以下阈值列表:

# 比率 # 比率
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()

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 中的字符串(记住,它是 "rgba(40, 40, 190, ratio)"),替换单词 “ratio” 为条目的 intersectionRatio。结果是:不仅颜色改变了,目标元素的透明度也改变了;随着相交度的下降,背景颜色的 alpha 值也随之下降,从而使元素变得更加透明。

最后,为了跟踪交集比率是上升还是下降,我们在变量 prevRatio 中记住当前比率。

结果

下面是产生的内容。向上和向下滚动这个页面,注意在你这样做的时候,盒子的外观如何变化。

<style>
#box {
  background-color: rgba(40, 40, 190, 255);
  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;
}
</style>

<div id="box">
  <div class="vertical">
    欢迎来到<strong>盒子!</strong>
  </div>
</div>

<script>
const numSteps = 20.0;

let boxElement;
let prevRatio = 0.0;
let increasingColor = "rgba(40, 40, 190, ratio)";
let decreasingColor = "rgba(190, 40, 40, ratio)";

window.addEventListener("load", (event) => {
  boxElement = document.querySelector("#box");

  createObserver();
}, false);

function createObserver() {
  let observer;

  let options = {
    root: null,
    rootMargin: "0px",
    threshold: buildThresholdList()
  };

  observer = new IntersectionObserver(handleIntersect, options);
  observer.observe(boxElement);
}

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;
}

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;
  });
}
</script>

尝试一下 »

使用 Intersection Observer API 对元素可见度进行计时 中有一个更广泛的实例。

规范

规范 状态 备注
Intersection Observer 工作草案 -

桌面浏览器兼容性

特性ChromeEdgeFirefoxInternet ExplorerOperaSafari
基础支持5115

55

53 — 55

不支持3812.1
IntersectionObserver() 构造函数5115

55

53 — 55

不支持3812.1
disconnect51151

55

53 — 55

不支持 支持 未知
observe5115

55

53 — 55

不支持 支持12.1
root5115

55

53 — 55

不支持 支持12.1
rootMargin5115

55

53 — 55

不支持 支持12.12
takeRecords51151

55

53 — 55

不支持 支持 未知
thresholds5115

55

53 — 55

不支持 支持12.1
unobserve51151

55

53 — 55

不支持 支持12.1

移动浏览器兼容性

特性AndroidChrome for AndroidEdge mobileFirefox for AndroidIE mobileOpera AndroidiOS Safari
基础支持5151 未知 未知 未知4112.2
IntersectionObserver() 构造函数5151 未知 未知 未知 未知12.2
disconnect5151 未知 未知 未知 未知 未知
observe5151 未知 未知 未知 未知12.2
root5151 未知 未知 未知 未知12.2
rootMargin5151 未知 未知 未知 未知12.22
takeRecords5151 未知 未知 未知 未知 未知
thresholds5151 未知 未知 未知 未知12.2
unobserve5151 未知 未知 未知 未知12.2

1. 自 Windows Insider Preview Build 14986 起可用

2. rootMargin 不适用于 <iframe>

相关链接