不知道你们有没有这个疑问,反正我有过。observe() 一个已经和根元素相交的目标元素之后,再也不滚动页面,意味着之后相交率再也不会变化,回调不应该发生,但还是发生了。这是因为:在执行 observe() 的时候,浏览器会将 previousThreshold 初始化成 0,而不是初始化成当前真正的相交率,然后在下次相交检测的时候就检测到相交率变化了,所以这种情况不是特殊处理。
浏览器何时进行相交检测,多久检测一次?
我们常见的显示器都是 60hz 的,就意味着浏览器每秒需要绘制 60 次(60fps),大概每 16.667ms 绘制一次。如果你使用 200hz 的显示器,那么浏览器每 5ms 就要绘制一次。我们把 16.667ms 和 5ms 这种每次绘制间隔的时间段,称之为 frame(帧,和 html 里的 frame 不是一个东西)。浏览器的渲染工作都是以这个帧为单位的,下图是 Chrome 中每帧里浏览器要干的事情(我在原图的基础上加了 Intersection Observations 阶段):
可以看到,相交检测(Intersection Observations)发生在 Paint 之后 Composite 之前,多久检测一次是根据显示设备的刷新率而定的。但可以肯定的是,每次绘制不同的画面之前,都会进行相交检测,不会有漏网之鱼。
一次性到达或跨过的多个临界值中选一个最近的
如果一个观察者实例设置了 11 个临界值:[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],那么当目标元素和根元素从完全不相交状态滚动到相交率为 1 这一段时间里,回调函数会触发几次?答案是:不确定。要看滚动速度,如果滚动速度足够慢,每次相交率到达下一个临界值的时间点都发生在了不同的帧里(浏览器至少绘制了 11 次),那么就会有 11 次相交被检测到,回调函数就会被执行 11 次;如果滚动速度足够快,从不相交到完全相交是发生在同一个帧里的,浏览器只绘制了一次,浏览器虽然知道这一次滚动操作就满足了 11 个指定的临界值(从不相交到 0,从 0 到 0.1,从 0.1 到 0.2 ··· ),但它只会考虑最近的那个临界值,那就是 1,回调函数只触发一次:
<div>相交次数: <span>0</span> <button>一下滚动到最低部</button> </div> <div></div> <style> #info { position: fixed; } #target { position: absolute; top: 200%; width: 100px; height: 100px; background: red; margin-bottom: 100px; } </style> <script> let observer = new IntersectionObserver(() => { times.textContent = +times.textContent + 1 }, { threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] // 11 个临界值 }) observer.observe(target) </script>
离开视口的时候也一个道理,假如根元素和目标元素的相交率先从完全相交变成了 0.45,然后又从 0.45 变成了完全不相交,那么回调函数只会触发两次。
如何判断当前是否相交?
我上面有几个 demo 都用了几行看起来挺麻烦的代码来判断目标元素是不是在视口内:
if (!target.isIntersecting) { // 相交 target.isIntersecting = true } else { // 不想交 target.isIntersecting = false }
为什么?难道用 entry.intersectionRatio > 0 判断不可以吗:
<div>不可见,请非常慢的向下滚动</div> <div></div> <style> #info { position: fixed; } #target { position: absolute; top: 200%; width: 100px; height: 100px; background: red; } </style> <script> let observer = new IntersectionObserver(([entry]) => { if (entry.intersectionRatio > 0) { // 快速滚动会执行到这里 info.textContent = "可见了" } else { // 慢速滚动会执行到这里 info.textContent = "不可见,请非常慢的向下滚动" } }) observer.observe(target) </script>
粗略一看,貌似可行,但你别忘了上面讲的贴边的情况,如果你滚动页面速度很慢,当目标元素的顶部和视口底部刚好挨上时,浏览器检测到相交了,回调函数触发了,但这时 entry.intersectionRatio 等于 0,会进入 else 分支,继续向下滚,回调函数再不会触发了,提示文字一直停留在不可见状态;但如果你滚动速度很快,当浏览器检测到相交时,已经越过了 0 那个临界值,存在了实际的相交面积,entry.intersectionRatio > 0 也就为 true 了。所以这样写会导致代码执行不稳定,不可行。