如何知道IntersectionObserver的滚动方向?

72

那么,当触发事件时,如何知道滚动的方向呢?

在返回的对象中,我看到最接近的可能性是与 boundingClientRect 交互,保存最后的滚动位置,但我不知道处理 boundingClientRect 是否会导致性能问题。

是否可以使用交叉事件来确定滚动方向(向上/向下)?

我添加了这个基本代码片段,如果有人能帮忙。
我将非常感激。

这是代码片段:

var options = {
  rootMargin: '0px',
  threshold: 1.0
}

function callback(entries, observer) { 
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('entry', entry);
    }
  });
};

var elementToObserve = document.querySelector('#element');
var observer = new IntersectionObserver(callback, options);

observer.observe(elementToObserve);
#element {
  margin: 1500px auto;
  width: 150px;
  height: 150px;
  background: #ccc;
  color: white;
  font-family: sans-serif;
  font-weight: 100;
  font-size: 25px;
  text-align: center;
  line-height: 150px;
}
<div id="element">Observed</div>

我希望了解这个,这样我就可以将其应用于固定标题菜单以展示或隐藏它。


6
“但我不确定处理boundingClientRect是否会导致性能问题。” - 当您使用IntersectionObserverEntry时,boundingClientRect已经被传递进去了,无论您是否要求。计算边框客户端矩形所涉及的任何“性能损失”已经发生,您无法改变这一点。因此,您最好利用已经计算出来的顶部或底部偏移量并将其与存储的先前值进行比较,而不是让已经做出的努力白费... - CBroe
6个回答

66
我不知道处理boundingClientRect是否会导致性能问题。
MDN指出,IntersectionObserver不在主线程上运行:
这样,网站就不再需要在主线程上执行任何操作来监视此类元素交叉,并且浏览器可以自由地优化交叉管理方式。 MDN,“Intersection Observer API” 我们可以通过保存IntersectionObserverEntry.boundingClientRect.y的值并将其与先前的值进行比较来计算滚动方向。
运行以下代码片段以获取示例:

const state = document.querySelector('.observer__state')
const target = document.querySelector('.observer__target')

const thresholdArray = steps => Array(steps + 1)
 .fill(0)
 .map((_, index) => index / steps || 0)

let previousY = 0
let previousRatio = 0

const handleIntersect = entries => {
  entries.forEach(entry => {
    const currentY = entry.boundingClientRect.y
    const currentRatio = entry.intersectionRatio
    const isIntersecting = entry.isIntersecting

    // Scrolling down/up
    if (currentY < previousY) {
      if (currentRatio > previousRatio && isIntersecting) {
        state.textContent ="Scrolling down enter"
      } else {
        state.textContent ="Scrolling down leave"
      }
    } else if (currentY > previousY && isIntersecting) {
      if (currentRatio < previousRatio) {
        state.textContent ="Scrolling up leave"
      } else {
        state.textContent ="Scrolling up enter"
      }
    }

    previousY = currentY
    previousRatio = currentRatio
  })
}

const observer = new IntersectionObserver(handleIntersect, {
  threshold: thresholdArray(20),
})

observer.observe(target)
html,
body {
  margin: 0;
}

.observer__target {
  position: relative;
  width: 100%;
  height: 350px;
  margin: 1500px 0;
  background: rebeccapurple;
}

.observer__state {
  position: fixed;
  top: 1em;
  left: 1em;
  color: #111;
  font: 400 1.125em/1.5 sans-serif;
  background: #fff;
}
<div class="observer__target"></div>
<span class="observer__state"></span>


如果你对{{thresholdArray}}辅助函数感到困惑,它会根据给定的步长构建一个从{{0.0}}到{{1.0}}范围内的数组。传递{{5}}将返回{{[0.0, 0.2, 0.4, 0.6, 0.8, 1.0]}}。

1
太棒了,你能解释一下这段代码吗?const thresholdArray = steps => Array(steps + 1) .fill(0) .map((_, index) => index / steps || 0) - Webwoman
6
这将创建一个有20个空槽的新数组,然后用零填充每个槽,再使用map()将索引除以20。这会产生0.05、0.1、0.15、0.2等结果,直到达到索引20,相当于1.0 [20/20]。这些是交点阈值点。 - Charles Robertson
3
你的if/else语句中条件设置不正确。你需要把isIntersecting变量作为“向上滚动离开”if语句的一个条件,并在其前添加逻辑运算符!。像这样:if (currentRatio < previousRatio && !isIntersecting)。你把isIntersecting变量放在了第一个else if语句中,这会在尝试向上滚动/离开时导致功能失效。该变量不需要在较高的条件中。不确定是否是疏忽还是错误。 - Xavier
我认为将boundingClientRect与IntersectionObserver一起使用不是一个好主意,因为IntersectionObserver的整个重点在于您不再需要boundingClientRect。如果您需要知道滚动方向,最好使用不那么昂贵的属性,例如window.pageYOffset。 - bitWorking
哎呀,现在我看到了。太棒了,那我什么也没说 ;) - bitWorking
显示剩余2条评论

22

这个解决方案没有使用任何外部状态,因此比跟踪其他变量的解决方案更简单:

const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.boundingClientRect.top < 0) {
          if (entry.isIntersecting) {
            // entered viewport at the top edge, hence scroll direction is up
          } else {
            // left viewport at the top edge, hence scroll direction is down
          }
        }
      },
      {
        root: rootElement,
      },
    );

1
非常好的解决方案,非常感谢!在我的情况下,我有几个小对象需要观察,所以我结合了您示例的中间部分,然后它就很好地工作了:if (entry.isIntersecting && entry.boundingClientRect.top < 0) { } else if (entry.isIntersecting && entry.boundingClientRect.top > 0) {} - Cenco
如果boundingClientRect大于视口,这个问题就会出现。我的解决方案不会受到这个问题的影响。 - oldboy

14

通过比较entryboundingClientRectrootBounds,您可以轻松知道目标是在视口上方还是下方。

callback()期间,您检查是否为isAbove/isBelow,然后最终将其存储到wasAbove/wasBelow中。下一次,如果目标出现在视口中(例如),则可以检查它是在上方还是下方。因此,您就知道它是从顶部还是底部进入的。

您可以尝试类似以下的代码:

var wasAbove = false;

function callback(entries, observer) {
    entries.forEach(entry => {
        const isAbove = entry.boundingClientRect.y < entry.rootBounds.y;

        if (entry.isIntersecting) {
            if (wasAbove) {
                // Comes from top
            }
        }

        wasAbove = isAbove;
    });
}

希望这有所帮助。


2
这在一些罕见情况下可能无法正常工作,例如当您从滚动位置(目标在视口下方)直接滚动到视口上方的滚动位置而没有使用动画时。由于在这两种情况下目标都不相交,因此浏览器不会触发回调。 - fabb

2

我的要求是:

  • 在向上滚动时不做任何操作
  • 在向下滚动时,确定一个元素是否从屏幕顶部开始隐藏

我需要查看 IntersectionObserverEntry 提供的一些信息:

  • intersectionRatio (应该从 1.0 减少)
  • boundingClientRect.bottom
  • boundingClientRect.height

因此回调看起来像这样:

intersectionObserver = new IntersectionObserver(function(entries) {
  const entry = entries[0]; // observe one element
  const currentRatio = intersectionRatio;
  const newRatio = entry.intersectionRatio;
  const boundingClientRect = entry.boundingClientRect;
  const scrollingDown = currentRatio !== undefined && 
    newRatio < currentRatio &&
    boundingClientRect.bottom < boundingClientRect.height;

  intersectionRatio = newRatio;

  if (scrollingDown) {
    // it's scrolling down and observed image started to hide.
    // so do something...
  }

  console.log(entry);
}, { threshold: [0, 0.25, 0.5, 0.75, 1] });

请查看我的帖子获取完整代码。


只有当元素滚动到页面上方时,答案才有效。根据我的测试,当元素从底部进入视图时,“scrollingDown”将为false。 - Rickard Elimää
@RickardElimää,你的观察似乎是正确的。我明确地说了,我的代码是一个捷径,用于捕捉图像滚动并开始隐藏的时刻,通过记忆比率。如果你对双向都感兴趣,你还需要监视Y值。 - bob

1

我认为使用单个阈值可能不太可能实现。您可以尝试观察intersectionRatio,在大多数情况下,当容器离开视口时,它的值通常低于1(因为交叉观察器是异步触发的)。但如果浏览器足够快,则它也可能是1。(我没有测试过:D)

但是,您或许可以使用多个值来观察两个阈值。:)

threshold: [0.9, 1.0]

如果您首先收到 0.9 的事件,那么容器进入视口就很清楚了...
希望这可以帮到您。 :)

“阈值”并不区分方向,而是基于“视口”,同时也取决于“rootMargin”,因此这种解决方案有点不稳定。此外,“0像素”高度的元素在触发事件时会有随机行为。我想结合“阈值”和“boundingClientRect”可能会起作用,我会尝试一下。 - Jose Paredes
啊...读错了。:D 请问滚动方向在你的情况下为什么很重要呢? - stefan judis
就像我在帖子中所说的那样!这样我就可以将其应用于固定标题菜单以显示/隐藏它。 - Jose Paredes
这与intersectionObserver实际工作的方式不相符。 - oldboy

0
最简单的方法是设置一个阈值为0.5的"threshold"值,它将元素的中心作为单个触发点,并监测"intersectionRect.y"。
当观察到元素时,如果"intersectionRect.y"为0,则表示元素"穿过"了边界框的顶部;而如果大于0,则表示元素"穿过"了边界框的底部。
您还必须监测先前的交叉(即通过"outside")以确定元素是在边界框内还是外部,因为如果元素退出边界框,"intersectionRect.y"的布尔值必须被反转。
const
  observer = new IntersectionObserver(observedArr => {
    let
      direction = outside ? observedArr[0].intersectionRect.x : !observedArr[0].inersectionRect.y
    if (direction) // to bottom
    else // to top
    outside = !outside
  }, { threshold: .5 })

let
  outside = false

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接