Intersection Observer 在一个循环中运行一个异步函数,用于检查观察到的 DOM 元素的位置。它与浏览器的渲染周期相结合,尽管它发生得非常快(大多数设备为 60fps,或每 16.66 毫秒一次),但如果您移动滚动条的速度比这些检查发生的速度更快,IO API 可能不会检测到一些可见性变化。事实上,没有被检测到的元素甚至都没有被渲染出来。
这是有道理的,因为 IO 的主要目标是检查一个元素是否对人眼可见。根据Intersection Observer spec 的说法,其目的是提供一种简单且最佳的解决方案来延迟或预加载图像和列表、检测电子商务广告的可见性等。
话虽如此,Intersection Observer 是一个期待已久的功能,它解决了很多问题,但它并不适合所有用例。如果您能接受在 高速滚动 情况下它不会捕获某些交叉点的事实 - 太好了。如果没有,那么,还有其他选择。
解决方案 1:推断相交元素
处理此问题的一种方法是尝试间接推断哪些元素穿过视口但未被 Intersection Observer 捕获。要实现它,您需要按升序为所有列表元素赋予唯一的数字属性。然后,在每个 Intersection Observer 的回调函数中,保存相交元素的最小和最大 ID。在回调结束时,调用setTimeout(applyChanges, 150) 来调度一个函数,该函数将循环遍历所有ID 介于min 和max 之间的元素,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?