【问题标题】:Intersection Observer fails sometimes when i scroll fast当我快速滚动时,Intersection Observer 有时会失败
【发布时间】:2020-09-09 01:54:06
【问题描述】:

我对 Intersection Observer API 有一点问题, 它对我有用,但是。

当我在网页上快速(非常快)滚动时,Intersection Observer API 有时无法检测到视口中元素的伪装。

当我缓慢/正常滚动时,它适用于所有元素

观察者选项:

RootMargin: 0px 0px -40px 0px,
threshold: 0.7,
root: null

元素的高度:在100px200px之间

有人知道为什么吗?

【问题讨论】:

  • 我没有对此进行大量测试,但避免添加 rootMargin,这是观察者在快速滚动时不会触发的原因。而是使用哨兵。在这种情况下,sentinel 是一个绝对定位的 div 元素,是您观察的对象。

标签: javascript intersection-observer


【解决方案1】:

Intersection Observer 在一个循环中运行一个异步函数,用于检查观察到的 DOM 元素的位置。它与浏览器的渲染周期相结合,尽管它发生得非常快(大多数设备为 60fps,或每 16.66 毫秒一次),但如果您移动滚动条的速度比这些检查发生的速度更快,IO API 可能不会检测到一些可见性变化。事实上,没有被检测到的元素甚至都没有被渲染出来。

这是有道理的,因为 IO 的主要目标是检查一个元素是否对人眼可见。根据Intersection Observer spec 的说法,其目的是提供一种简单且最佳的解决方案来延迟或预加载图像和列表、检测电子商务广告的可见性等。

话虽如此,Intersection Observer 是一个期待已久的功能,它解决了很多问题,但它并不适合所有用例。如果您能接受在 高速滚动 情况下它不会捕获某些交叉点的​​事实 - 太好了。如果没有,那么,还有其他选择。

解决方案 1:推断相交元素

处理此问题的一种方法是尝试间接推断哪些元素穿过视口但未被 Intersection Observer 捕获。要实现它,您需要按升序为所有列表元素赋予唯一的数字属性。然后,在每个 Intersection Observer 的回调函数中,保存相交元素的最小和最大 ID。在回调结束时,调用setTimeout(applyChanges, 150) 来调度一个函数,该函数将循环遍历所有ID 介于minmax 之间的元素,IO 没有省略它。另外,将clearTimeout()放在回调的开头,以确保该函数等待一段时间,直到IO处于非活动状态。

let minId = null;
let maxId = null;
let debounceTimeout = null;

function applyChanges() {
  console.log(minId, maxId);
  const items = document.querySelectorAll('.item');
  // perform action on elements with Id between min and max
  minId = null;
  maxId = null;
}

function reportIntersection(entries) {
  clearTimeout(debounceTimeout);
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const entryId = parseInt(entry.target.id);
      if (minId === null || maxId === null) {
        minId = entryId;
        maxId = entryId;
      } else {
        minId = Math.min(minId, entryId);
        maxId = Math.max(maxId, entryId);
      }
    }
  });
  debounceTimeout = setTimeout(applyChanges, 500);
}

const container = document.querySelector('#container');
const items = document.querySelectorAll('.item');
const io = new IntersectionObserver(reportIntersection, container);
let idCounter = 0;
items.forEach(item => {
  item.setAttribute('id', idCounter++);
  io.observe(item)
});

解决方案 2:限制滚动事件

不可能影响默认滚动条的行为,但您可以使用自定义滚动条覆盖它。这个想法是限制滚动事件,以便容器在每个渲染周期都不能滚动超过它自己的高度。这样,IntersectionObserver 将有时间捕获所有路口。虽然我建议使用现有的库来完成这项任务,但下面是它应该如何工作的粗略逻辑。

首先,定义容器在单个渲染周期内可以滚动的最大距离:let scrollPool = 500。在每个滚动事件上,安排在requestAnimationFrame() 函数的下一次渲染期间将scrollPool 重置为原始值。接下来,检查scrollPool 是否小于0,如果是,则return 不滚动。否则,从scrollPool 中减去scrollDistance 并将scrollDistance 添加到容器的scrollTop 属性中。

同样,由于用户可以通过多种方式滚动页面,因此实施此解决方案可能有点过头了。但为了描述这个想法,这里有一个wheel 事件的草稿:

#container { overflow: hidden; }
let scrollPool = 500;

function resetScrollPool() {
  scrollPool = 500;
}

function scrollThrottle(event) {
  window.requestAnimationFrame(resetScrollPool);
  if (scrollPool < 0) {
    return false;
  }
  const scrollDistance = event.deltaY * 10;
  scrollPool = scrollPool - Math.abs(scrollDistance);
  document.querySelector('#container').scrollTop += scrollDistance;
}

window.addEventListener('wheel', scrollThrottle);

解决方案 3:实施自定义交叉口检测

这意味着完全放弃IntersectionObserver 并使用旧方法。这在性能方面肯定会更加昂贵,但您一定会捕获所有穿过视口的元素。这个想法是添加一个滚动处理函数来循环遍历容器的所有子项,以查看它们是否越过可见区域。为了提高性能,您可能希望将此函数反跳一秒左右。一个很好的实现可以在:How can I tell if a DOM element is visible in the current viewport?

【讨论】:

  • 不错!选项 1 听起来是最好的。
  • 对于选项 1:您必须添加一个连续的 id 标签,否则 entryId 基本上是空的,一种规避方法是在此代码本身中添加一个:let cntr = 0; items.forEach(item => {item.setAttribute('id', cntr++);io.observe(item);});
  • @AG 感谢您的建议。我已将其包含在代码示例中。干杯
  • 其他解决方案:利用用户尝试与似乎不起作用的东西进行交互的倾向,以强制您的功能在某些用户交互上运行。就我而言,我在mouseenterclick 上添加了一个事件侦听器,并且效果很好。人们会说“这很慢,怎么了?”如果他们还没有在滚动过程中,他们几乎普遍至少会将鼠标光标放在它上面。由于这种情况很少见,所以对我来说已经足够了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-03-15
  • 1970-01-01
相关资源
最近更新 更多