使用 Intersection Observer API 定时元素可见性

在本文中,我们将构建一个模拟博客,其中包含许多散布在页面内容中的广告,然后使用Intersection Observer API跟踪每个广告对用户的可见时间。当广告的可见时间超过一分钟时,它将被替换为新的广告。

虽然此示例的许多方面与现实世界的用法不符(特别是,所有文章都具有相同的文本并且不是从数据库加载的,并且只有一小部分简单的纯文本广告是从数组中选择的),但这应该足以了解 API 以快速学习如何将 Intersection Observer API 应用于您自己的网站。

跟踪广告可见性的概念在该示例中使用是有充分理由的。事实证明,在 Web 上的广告中,使用 Flash 或其他脚本最常见的用途之一是记录每个广告的可见时间,以便进行计费和收入结算。如果没有 Intersection Observer API,这最终将使用每个广告的间隔和超时来完成,或者使用其他倾向于降低页面速度的技术。使用此 API 使浏览器能够简化所有内容,从而大幅降低对性能的影响。

让我们开始吧!

构建网站

网站结构:HTML

网站的结构并不复杂。我们将使用CSS Grid来设计和布局网站,因此我们可以在这里做到非常直接

html
<div class="wrapper">
  <header>
    <h1>A Fake Blog</h1>
    <h2>Showing Intersection Observer in action!</h2>
  </header>

  <aside>
    <nav>
      <ul>
        <li><a href="#link1">A link</a></li>
        <li><a href="#link2">Another link</a></li>
        <li><a href="#link3">One more link</a></li>
      </ul>
    </nav>
  </aside>

  <main></main>
</div>

这是整个网站的框架。顶部是网站的页眉区域,包含在<header>块中。在下面,我们将网站侧边栏定义为一个包含在<aside>块中的链接列表。

最后是主体。我们从这里开始一个空的<main>元素。此框将在稍后使用脚本填充。

使用 CSS 样式化网站

定义了网站的结构后,我们转向网站的样式。让我们分别看看每个页面组件的样式。

基础

我们为<body><main>元素提供了样式,以定义网站的背景以及网站各个部分将放置的网格。

css
body {
  font-family: "Open Sans", "Arial", "Helvetica", sans-serif;
  background-color: aliceblue;
}

.wrapper {
  display: grid;
  grid-template-columns: auto minmax(min-content, 1fr);
  grid-template-rows: auto minmax(min-content, 1fr);
  max-width: 700px;
  margin: 0 auto;
  background-color: aliceblue;
}

网站的<body>在此处配置为使用许多常见无衬线字体之一,并使用"aliceblue"作为背景色。然后定义"wrapper"类;它包裹着整个博客,包括页眉、侧边栏和正文内容(文章和广告)。

wrapper 建立了一个包含两列和两行的 CSS 网格。第一列(根据其内容自动调整大小)用于侧边栏,第二列(将用于正文内容)的大小至少为列内容的宽度,最多为所有剩余可用空间。

第一行将专门用于网站页眉。行的尺寸与列的尺寸相同:第一行自动调整大小,第二行使用剩余空间,但至少有足够的空间容纳其中的所有元素。

wrapper 的宽度固定为 700px,因此它在 MDN 下方内联显示时将适合可用空间。

页眉

页眉非常简单,因为对于此示例,它只包含一些文本。它的样式如下所示

css
header {
  grid-column: 1 / -1;
  grid-row: 1;
  background-color: aliceblue;
}

grid-row 设置为 1,因为我们希望页眉放置在网站网格的顶行。更有趣的是我们在这里使用grid-column;在这里我们指定希望列从第一列开始,并在最后一行网格后的第一列结束——换句话说,页眉跨越网格中的所有列。完美符合我们的需求。

侧边栏

我们的侧边栏用于显示指向网站上其他页面的链接。在我们的示例中,它们都不起作用,但它们的存在是为了帮助提供类似博客的体验。侧边栏使用<aside>元素表示,并按如下方式设置样式

css
aside {
  grid-column: 1;
  grid-row: 2;
  background-color: cornsilk;
  padding: 5px 10px;
}

aside ul {
  padding-left: 0;
}

aside ul li {
  list-style: none;
}

aside ul li a {
  text-decoration: none;
}

这里最重要的是,grid-column 设置为 1,将侧边栏放置在屏幕左侧。如果您将其更改为 -1,它将出现在右侧(尽管其他一些元素可能需要对边距进行一些调整才能使间距正确)。grid-row 设置为 2,将其放置在网站主体旁边。

内容主体

说到网站的主体:网站的主要内容保留在一个<main>元素中。以下样式应用于该元素

css
main {
  grid-column: 2;
  grid-row: 2;
  margin: 0;
  margin-left: 16px;
  font-size: 16px;
}

这里的主要功能是网格位置设置为将正文内容放置在第 2 列,第 2 行。

文章

每篇文章都包含在一个<article>元素中,其样式如下所示

css
article {
  background-color: white;
  padding: 6px;
}

article:not(:last-child) {
  margin-bottom: 8px;
}

article h2 {
  margin-top: 0;
}

这将创建具有白色背景的文章框,这些框浮动在蓝色背景之上,文章周围有小边距。容器中不是最后一项的每篇文章都具有 8px 底部边距,以将内容隔开。

广告

最后,广告具有以下初始样式。单个广告可能会在一定程度上自定义样式,我们将在后面看到。

css
.ad {
  height: 96px;
  padding: 6px;
  border-color: #555;
  border-style: solid;
  border-width: 1px;
}

.ad:not(:last-child) {
  margin-bottom: 8px;
}

.ad h2 {
  margin-top: 0;
}

.ad div {
  position: relative;
  float: right;
  padding: 0 4px;
  height: 20px;
  width: 120px;
  font-size: 14px;
  bottom: 30px;
  border: 1px solid black;
  background-color: rgb(255 255 255 / 50%);
}

这里没有任何神奇之处。它非常基本的 CSS。

使用 JavaScript 结合在一起

这将我们带到了使一切工作的 JavaScript 代码。让我们从全局变量开始

js
let contentBox;

let nextArticleID = 1;
const visibleAds = new Set();
let previouslyVisibleAds = null;

let adObserver;
let refreshIntervalID = 0;

这些的用途如下

contentBox

对 DOM 中<main>元素的HTMLElement对象的引用。我们将在此处插入文章和广告。

nextArticleID

每篇文章都将获得一个唯一的 ID 号码;此变量跟踪要使用的下一个 ID,从 1 开始。

visibleAds

一个Set,我们将使用它来跟踪当前显示在屏幕上的广告。

previouslyVisibleAds

用于在文档不可见时临时存储可见广告列表(例如,如果用户已切换到另一个页面)。

adObserver

将保存我们的IntersectionObserver,用于跟踪广告与<main>元素边界的交叉。

refreshIntervalID

用于存储setInterval() 返回的间隔 ID。此间隔将用于触发我们的广告内容的定期刷新。

设置

为了设置,我们在页面加载时运行下面的startup()函数

js
window.addEventListener("load", startup, false);

function startup() {
  contentBox = document.querySelector("main");

  document.addEventListener("visibilitychange", handleVisibilityChange, false);

  const observerOptions = {
    root: null,
    rootMargin: "0px",
    threshold: [0.0, 0.75],
  };

  adObserver = new IntersectionObserver(intersectionCallback, observerOptions);

  buildContents();
  refreshIntervalID = setInterval(handleRefreshInterval, 1000);
}

首先,获取对内容包装<main>元素的引用,以便我们可以将内容插入其中。然后,我们为visibilitychange事件设置事件监听器。当文档变为隐藏或可见时(例如,当用户在浏览器中切换标签页时)会发送此事件。Intersection Observer API 在检测交叉时不会考虑这一点,因为交叉不受页面可见性的影响。因此,我们需要在页面被切换出去时暂停我们的计时器;因此有了这个事件监听器。

接下来,我们为IntersectionObserver 设置选项,该选项将监控目标元素(在本例中为广告)以查找相对于文档的交叉更改。这些选项配置为监视与文档视窗的交叉(通过将root设置为null)。我们没有任何边距来扩展或收缩交叉根的矩形;我们希望完全匹配文档视窗的边界以进行交叉。并且threshold设置为包含值 0.0 和 0.75 的数组;这将导致我们的回调在目标元素完全被遮挡或开始被遮挡(交叉比率 0.0)或通过 75% 可见(交叉比率 0.75)时执行。

观察者adObserver是通过调用IntersectionObserver的构造函数创建的,将回调函数intersectionCallback和我们的选项传递给它。

然后,我们调用一个函数buildContents(),我们将在后面定义它,以便实际生成我们想要呈现的文章和广告并将其插入文档中。

最后,我们设置一个间隔,每秒触发一次,以处理任何必要的刷新。我们需要一秒钟刷新一次,因为出于本示例的目的,我们在所有可见广告中显示计时器。您可能根本不需要间隔,或者您可以使用不同的方式或使用不同的时间间隔来完成。

处理文档可见性更改

让我们看一下visibilitychange事件的处理程序。当文档本身变为可见或不可见时,我们的脚本将收到此事件。这里最重要的场景是用户切换标签页。由于 Intersection Observer 仅关心目标元素与交叉根的交叉,而不是标签的可见性(这是一个完全不同的问题),因此我们需要使用Page Visibility API来检测这些标签切换并禁用我们的计时器在整个过程中。

js
function handleVisibilityChange() {
  if (document.hidden) {
    if (!previouslyVisibleAds) {
      previouslyVisibleAds = visibleAds;
      visibleAds = [];
      previouslyVisibleAds.forEach((adBox) => {
        updateAdTimer(adBox);
        adBox.dataset.lastViewStarted = 0;
      });
    }
  } else {
    previouslyVisibleAds.forEach((adBox) => {
      adBox.dataset.lastViewStarted = performance.now();
    });
    visibleAds = previouslyVisibleAds;
    previouslyVisibleAds = null;
  }
}

由于事件本身没有说明文档是已从可见切换到不可见还是从不可见切换到可见,因此检查document.hidden属性以查看文档当前是否不可见。由于理论上可能被多次调用,因此我们只有在我们尚未暂停计时器并保存现有广告的可见性状态时才继续执行。

要暂停计时器,我们只需要从可见广告集(visibleAds)中移除广告并将其标记为非活动状态。为此,我们首先将可见广告集保存到一个名为previouslyVisibleAds的变量中,以确保我们可以在用户切换回文档时恢复它们,然后清空visibleAds集,这样它们就不会被视为可见。然后,对于每个被暂停的广告,我们调用updateAdTimer()函数,该函数负责更新广告的总可见时间计数器,然后我们将它们的dataset.lastViewStarted属性设置为0,这表示标签的计时器没有运行。

如果文档刚刚变为可见,我们将逆转此过程:首先遍历previouslyVisibleAds,并使用performance.now()方法将每个广告的dataset.lastViewStarted设置为当前文档的时间(以文档创建以来的毫秒数表示)。然后我们将visibleAds恢复为previouslyVisibleAds,并将后者设置为null。现在所有广告都重新启动,并配置为知道它们在当前时间变得可见,因此在下次更新时,它们不会累积页面被切换到后台的时间长度。

处理交叉变化

在浏览器事件循环的每次循环中,每个IntersectionObserver都会检查其目标元素是否已穿过任何观察者的交叉比率阈值。对于每个观察者,都会编译一个包含已穿过阈值的元素列表,并将该列表作为IntersectionObserverEntry对象的数组发送到观察者的回调函数。我们的回调函数intersectionCallback()如下所示:

js
function intersectionCallback(entries) {
  entries.forEach((entry) => {
    const adBox = entry.target;

    if (entry.isIntersecting) {
      if (entry.intersectionRatio >= 0.75) {
        adBox.dataset.lastViewStarted = entry.time;
        visibleAds.add(adBox);
      }
    } else {
      visibleAds.delete(adBox);
      if (
        entry.intersectionRatio === 0.0 &&
        adBox.dataset.totalViewTime >= 60000
      ) {
        replaceAd(adBox);
      }
    }
  });
}

如前所述,IntersectionObserver回调函数接收一个包含所有目标元素的数组,这些元素的可见度比交叉观察者比率中的一个更高或更低。我们迭代每个条目,这些条目是IntersectionObserverEntry类型的。如果目标元素与根元素相交,我们就知道它刚刚从遮挡状态转换到可见状态。如果它至少可见75%,那么我们将广告视为可见,并通过将广告的dataset.lastViewStarted属性设置为转换时间(以entry.time表示)来启动计时器,然后将广告添加到visibleAds集中,以便我们在时间推移时知道要对其进行处理。

如果广告已转换为非相交状态,我们将从可见广告集中移除该广告。然后我们有一个特殊的行为:我们查看entry.intersectionRatio是否为0.0;如果是,则表示元素已完全遮挡。如果是这种情况,并且广告总共已可见至少一分钟,我们将调用一个名为replaceAd()的函数,以将现有广告替换为新广告。这样,用户会随着时间的推移看到各种广告,但广告仅在用户无法看到时才被替换,从而带来流畅的体验。

处理周期性操作

我们的间隔处理程序handleRefreshInterval()每秒大约被调用一次,这是由于在startup()函数中调用了setInterval()所致,如上所述。它的主要工作是每秒更新一次计时器,并安排一个重绘操作以更新我们将在每个广告中绘制的计时器。

js
function handleRefreshInterval() {
  const redrawList = [];

  visibleAds.forEach((adBox) => {
    const previousTime = adBox.dataset.totalViewTime;
    updateAdTimer(adBox);

    if (previousTime !== adBox.dataset.totalViewTime) {
      redrawList.push(adBox);
    }
  });

  if (redrawList.length) {
    window.requestAnimationFrame((time) => {
      redrawList.forEach((adBox) => {
        drawAdTimer(adBox);
      });
    });
  }
}

数组redrawList将用于保存所有需要在此刷新周期中重绘的广告的列表,因为由于系统活动或您将间隔设置为除每 1000 毫秒之外的其他值,它可能与经过的时间不完全相同。

然后,对于每个可见广告,我们保存dataset.totalViewTime的值(广告当前可见的总毫秒数,截至上次更新时间),然后调用updateAdTimer()以更新时间。如果已更改,我们将广告推送到redrawList中,以便我们在下一个动画帧中知道需要对其进行更新。

最后,如果有至少一个元素需要重绘,我们将使用requestAnimationFrame()来安排一个函数,该函数将在下一个动画帧中重绘redrawList中的每个元素。

更新广告的可见度计时器

之前(参见处理文档可见度更改处理周期性操作),我们已经看到,当我们需要更新广告的“总可见时间”计数器时,我们会调用一个名为updateAdTimer()的函数来执行此操作。此函数接收广告的HTMLDivElement对象作为输入。它如下所示:

js
function updateAdTimer(adBox) {
  const lastStarted = adBox.dataset.lastViewStarted;
  const currentTime = performance.now();

  if (lastStarted) {
    const diff = currentTime - lastStarted;

    adBox.dataset.totalViewTime =
      parseFloat(adBox.dataset.totalViewTime) + diff;
  }

  adBox.dataset.lastViewStarted = currentTime;
}

为了跟踪元素的可见时间,我们对每个广告使用两个自定义数据属性(参见data-*):

lastViewStarted

以毫秒为单位的时间,相对于文档创建的时间,该时间是广告的可见度计数最后一次更新的时间,或者广告最后一次变为可见的时间。如果广告在上次检查时不可见,则为0。

totalViewTime

广告已可见的总毫秒数。

这些属性通过每个广告的HTMLElement.dataset属性访问,该属性提供一个DOMStringMap,将每个自定义属性的名称映射到其值。这些值是字符串,但我们可以轻松地将它们转换为数字——实际上,JavaScript 通常会自动执行此操作,尽管我们有一个实例需要我们自己执行此操作。

我们首先将广告上次可见度状态检查时间(adBox.dataset.lastViewStarted)的时间值提取到一个名为lastStarted的局部变量中。我们还使用performance.now()将创建以来的当前时间值提取到currentTime中。

如果lastStarted非零(表示计时器当前正在运行),我们将计算当前时间与开始时间之间的差值,以确定计时器自上次变为可见以来的可见毫秒数。这将添加到广告的totalViewTime的当前值中,以更新总值。请注意此处使用parseFloat();因为这些值是字符串,所以 JavaScript 会尝试执行字符串连接而不是加法,如果不使用它则会导致错误。

最后,广告的上次查看时间更新为当前时间。无论广告在调用此函数时是否正在运行,都会执行此操作;这会导致广告的计时器在该函数返回时始终处于运行状态。这是有道理的,因为此函数仅在广告可见时才调用,即使它刚刚变为可见。

绘制广告计时器

在每个广告内部,出于演示目的,我们将绘制其totalViewTime的当前值(转换为分钟和秒)。这可以通过将广告元素传递到drawAdTimer()函数中来完成。

js
function drawAdTimer(adBox) {
  const timerBox = adBox.querySelector(".timer");
  const totalSeconds = adBox.dataset.totalViewTime / 1000;
  const sec = Math.floor(totalSeconds % 60);
  const min = Math.floor(totalSeconds / 60);

  timerBox.innerText = `${min}:${sec.toString().padStart(2, "0")}`;
}

此代码使用其 ID "timer"查找广告的计时器,并通过将广告的totalViewTime除以 1000 来计算经过的秒数。然后它计算经过的分钟数和秒数,然后将计时器的innerText设置为以 m:ss 格式表示该时间的字符串。String.padStart()方法用于确保秒数在小于 10 时填充为两位数。

构建页面内容

buildContents()函数由启动代码调用,以选择要呈现的文章和广告并将其插入文档中。

js
const loremIpsum =
  "<p>Lorem ipsum dolor sit amet, consectetur adipiscing" +
  " elit. Cras at sem diam. Vestibulum venenatis massa in tincidunt" +
  " egestas. Morbi eu lorem vel est sodales auctor hendrerit placerat" +
  " risus. Etiam rutrum faucibus sem, vitae mattis ipsum ullamcorper" +
  " eu. Donec nec imperdiet nibh, nec vehicula libero. Phasellus vel" +
  " malesuada nulla. Aliquam sed magna aliquam, vestibulum nisi at," +
  " cursus nunc.</p>";

function buildContents() {
  for (let i = 0; i < 5; i++) {
    contentBox.appendChild(createArticle(loremIpsum));

    if (!(i % 2)) {
      loadRandomAd();
    }
  }
}

变量loremIpsum包含我们将用于所有文章正文的文本。显然,在现实世界中,您将有一些代码从数据库或类似位置拉取文章,但这对于我们的目的来说已经足够了。每篇文章都使用相同的文本;您当然可以轻松地更改它。

buildContents()创建一个包含五篇文章的页面。在每个奇数编号的文章之后,都会“加载”一个广告并将其插入页面中。文章在使用名为createArticle()的方法创建后被插入到内容框(即包含所有网站内容的<main>元素)中,我们将在下面介绍该方法。

广告是使用名为loadRandomAd()的函数创建的,该函数既创建广告又将其插入页面。我们将在后面看到,同一个函数也可以替换现有广告,但就目前而言,我们正在将广告追加到现有内容中。

创建文章

为了创建文章的<article>元素(及其所有内容),我们使用createArticle()函数,该函数接收一个字符串作为输入,该字符串是添加到页面的文章的完整文本。

js
function createArticle(contents) {
  const articleElem = document.createElement("article");
  articleElem.id = nextArticleID;

  const titleElem = document.createElement("h2");
  titleElem.innerText = `Article ${nextArticleID} title`;
  articleElem.appendChild(titleElem);

  articleElem.innerHTML += contents;
  nextArticleID += 1;

  return articleElem;
}

首先,创建<article>元素,并将它的 ID 设置为唯一的nextArticleID值(该值从 1 开始,每篇文章递增)。然后我们创建并追加文章标题的h2元素,然后将contents中的 HTML 追加到该元素。最后,nextArticleID递增(以便下一个元素获得新的唯一 ID),并将新创建的<article>元素返回给调用者。

创建广告

loadRandomAd()函数模拟加载广告并将其添加到页面中。如果您没有为replaceBox传递值,则会创建一个新元素来包含广告;然后将广告追加到页面中。如果您指定了replaceBox,则该框将被视为现有广告元素;它不会创建新的元素,而是将现有元素更改为包含新广告的样式、内容和其他数据。这避免了在更新广告时进行冗长的布局工作的风险,这可能在您首先删除旧元素然后插入新元素时发生。

js
function loadRandomAd(replaceBox) {
  const ads = [
    {
      bgcolor: "#cec",
      title: "Eat Green Beans",
      body: "Make your mother proud—they're good for you!",
    },
    {
      bgcolor: "aquamarine",
      title: "MillionsOfFreeBooks.whatever",
      body: "Read classic literature online free!",
    },
    {
      bgcolor: "lightgrey",
      title: "3.14 Shades of Gray: A novel",
      body: "Love really does make the world go round…",
    },
    {
      bgcolor: "#fee",
      title: "Flexbox Florist",
      body: "When life's layout gets complicated, send flowers.",
    },
  ];
  let adBox, title, body, timerElem;

  const ad = ads[Math.floor(Math.random() * ads.length)];

  if (replaceBox) {
    adObserver.unobserve(replaceBox);
    adBox = replaceBox;
    title = replaceBox.querySelector(".title");
    body = replaceBox.querySelector(".body");
    timerElem = replaceBox.querySelector(".timer");
  } else {
    adBox = document.createElement("div");
    adBox.className = "ad";
    title = document.createElement("h2");
    body = document.createElement("p");
    timerElem = document.createElement("div");
    adBox.appendChild(title);
    adBox.appendChild(body);
    adBox.appendChild(timerElem);
  }

  adBox.style.backgroundColor = ad.bgcolor;

  title.className = "title";
  body.className = "body";
  title.innerText = ad.title;
  body.innerHTML = ad.body;

  adBox.dataset.totalViewTime = 0;
  adBox.dataset.lastViewStarted = 0;

  timerElem.className = "timer";
  timerElem.innerText = "0:00";

  if (!replaceBox) {
    contentBox.appendChild(adBox);
  }

  adObserver.observe(adBox);
}

首先是数组ads。此数组包含创建每个广告所需的数据。我们这里有四个广告可供随机选择。当然,在现实世界中,广告会来自数据库或更可能是来自使用 API 提取广告的广告服务。但是,我们的需求很简单:每个广告都由一个包含三个属性的对象表示:背景颜色(bgcolor)、标题(title)和正文文本字符串(body)。

然后我们定义了几个变量:

adBox

这将被设置为表示广告的元素。对于追加到页面的新广告,这是使用Document.createElement()创建的。在替换现有广告时,这将被设置为指定的广告元素(replaceBox)。

title

将保存表示广告标题的h2元素。

body

将保存表示广告正文文本的<p>

timerElem

将保存包含广告显示时间信息的<div>元素。

通过计算Math.floor(Math.random() * ads.length)选择一个随机广告;结果是0到广告数量减1之间的值。相应的广告现在被称为adBox

如果为replaceBox指定了值,我们将使用该值作为广告元素。为此,我们首先通过调用IntersectionObserver.unobserve()来结束对元素的观察。然后,每个构成广告的元素的局部变量:广告框本身、标题、正文和计时器框,都设置为现有广告中相应元素的值。

如果未为replaceBox指定值,我们将创建一个新的广告元素。广告的新<div>元素被创建,并通过将它的类名设置为"ad"来设置它的属性。接下来,创建广告标题元素,以及正文和可见性计时器;它们分别是h2<p><div>元素。这些元素被追加到adBox元素。

之后,代码路径再次汇合。广告的背景颜色被设置为新广告记录中指定的值,元素的类和内容也被相应地设置。

接下来,是时候通过将adBox.dataset.totalViewTimeadBox.dataset.lastViewStarted设置为0来设置自定义数据属性以跟踪广告的可见性数据。

最后,我们设置了将显示计时器的<div>的ID,该计时器将显示在广告中以显示它已经可见的时间,并将其设置为类名"timer"。初始文本设置为“0:00”,表示0分钟0秒的开始时间,并将其追加到广告中。

如果我们没有替换现有广告,我们需要使用Document.appendChild()将元素追加到页面的内容区域。如果我们正在替换广告,它已经存在了,它的内容将被替换为新广告的内容。然后,我们在我们的 Intersection Observer,adObserver 上调用observe()方法,以开始观察广告在其与视窗的交叉点的变化。从现在开始,无论广告何时被完全遮挡或即使单个像素变得可见,或者广告以某种方式通过75%可见,观察者的回调都会被执行。

替换现有广告

我们的观察者的回调会留意那些被完全遮挡并且总可见时间至少为一分钟的广告。当这种情况发生时,replaceAd()函数将以该广告的元素作为输入调用,以便将旧广告替换为新广告。

js
function replaceAd(adBox) {
  updateAdTimer(adBox);

  const visibleTime = adBox.dataset.totalViewTime;
  console.log(
    `Replacing ad: ${
      adBox.querySelector("h2").innerText
    } - visible for ${visibleTime}`,
  );

  loadRandomAd(adBox);
}

replaceAd()首先在现有广告上调用updateAdTimer(),以确保其计时器是最新的。这确保了当我们读取它的totalViewTime时,我们会看到广告对用户可见的确切最终值。然后我们报告该数据;在这种情况下,是通过将其记录到控制台,但在现实世界中,你会将信息提交到广告服务的 API 或将其保存到数据库中。

然后,我们通过调用loadRandomAd()来加载一个新广告,并指定要替换的广告作为输入参数。正如我们之前看到的,loadRandomAd()将使用与新广告相对应的内容和数据替换现有广告,如果你指定现有广告的元素作为输入参数。

新广告的元素对象将返回给调用方,以备不时之需。

结果

生成的页面如下所示。尝试通过向上和向下滚动来进行试验,并注意可见性的变化如何影响每个广告中的计时器。还要注意,每个广告在可见一分钟后都会被替换(但广告必须首先被滚动出视图,然后再滚动回来),以及计时器如何在文档被切换到后台时暂停。但是,用另一个窗口覆盖浏览器不会暂停计时器。

另请参见