IntersectionObserver

IntersectionObserver

最近发现一个有趣的API,实际上这个api已经面世好几年了,但最初浏览器兼容性有限,加上此功能使用场景不频繁,且有成熟的替代方案,所以知道的人并不多。

这个接口的功能,简单来说就是:监测页面中的某个元素的可视状态。可以是该元素相对于整个视窗的可见性,也可以是相对于某个父元素的可见性。我们平时使用较多的场景大概有:

举个栗子:

  1. 无限加载:滑动到底部时开始加载下页
  2. 吸顶效果:在页面滑动时触发某元素的悬浮效果(如下滑到一定位置,顶栏悬浮固定)

目前我们常用的成熟方案是,监听页面/元素的scroll事件,在scroll事件中通过判断元素的位置,来进行相应的逻辑处理。或者是分别通过touchstart touchmove touchend 等触摸事件,来判断滑动方向,滑动距离,再做出相应处理。主流的一些相关功能库基本也是这样的实现原理,如iscroll.js等。

从微信小程序的一个bug说起说起

这些方案使用上并没有太明显的问题,然而最近在开发小程序时,有一个滑动吸顶的效果,需要根据滑动距离变换顶部的背景,采用的便是上述的方案,监听scroll-view的滚动事件,根据元素距离顶部的距离实时判断修改。

为了提高性能,多滚动事件采取了节流,在此过程中发现小程序的一个问题,那就是采用节流后,如果滑动过快,小程序bindscroll事件返回的组件距离顶部位置会有错误,比如我从距顶50像素迅速滑到0,此时回调函数返回的任然是50。试验了下跟节流时间关系不大,即便设置几毫秒的节流间隔,已然存在这个问题。而如果不设置节流,在低端机,尤其安卓机上频繁触发,导致页面相当卡顿。

最后搜索半天,在微信开发者社区发现了原因所在。根据官方给出的解决方案,给scroll-view组件增加一个属性throttle="{{false}}"(官方文档中并无此属性),原因是官方的组件在配发滑动事件时,默认开启了节流,在滑动过快时,有部分事件可能被节流掉,就出现了上述情况,去掉官方节流,保证事件都能触发,然后自己再实现节流即可。

1
2
3
<scroll-view
throttle="{{false}}"
bindscroll="handleScroll">

说道半途记起的一个bug,回归正题。
使用监听scrool事件的方法,不是用节流肯定是不行的,节流事件如果太短,那么节流效果不明显,时间太长的话,事件响应又不够及时。经过权衡设置了大概50~100ms的延时,让事件的配发频率和响应延时都能接受,但效果任然不够理想,达不到极致的体验。理想情况下,我们追求:响应及时、性能负担小。

无意间看到微信的一个新的api wx.createIntersectionObserver,简单了解后不明觉厉,然后发现浏览器端也有对应的IntersectionObserver API(“交叉观察器”)。由此引出一种新的更完美的解决方案,即抛弃scroll事件,采用新的“交叉观察器”

scroll事件的一些缺点:
1、响应密集,会造成性能问题
2、为解决1,采用节流后响应不够及时
3、实现某些需求较为复杂:如,当视频滑到可视区域时自动播放等,需要判断列表中某元素的位置,较为复杂
4、统计列表中个元素的曝光量(这个用scroll时间监听的话感觉头都裂了)

IntersectionObserver

MDN doc介绍:

1
IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。

属性:

  • IntersectionObserver.root

    要监听元素的父级窗口,及我要监听的元素,是相对于那个视窗的可见性,是整个全屏的视窗,还是页面中某个容器。不传默认为全局的视窗

  • IntersectionObserver.rootMargin

    相对于视窗的偏移量

  • IntersectionObserver.thresholds

    阈值的列表, 按升序排列。阈值指所监听元素的可视比例,全部不可见为0,全部可见为1,配置此属性后,会在到达对应阈值后触发回调事件,如[0.1, 0.4, 1] 表示在可视比例达到10%、40%、100%时,会分别相应回调事件;如果不设置,默认是在元素全显和全隐时分别触发。

方法:

  • IntersectionObserver.disconnect()

    使IntersectionObserver对象停止监听工作。

  • IntersectionObserver.observe()

    使IntersectionObserver开始监听一个目标元素。

  • IntersectionObserver.takeRecords()

    返回所有观察目标的IntersectionObserverEntry对象数组。

  • IntersectionObserver.unobserve()

    使IntersectionObserver停止监听特定目标元素。

示例

1
2
3
4
5
6
7
8
9
10
var intersectionObserver = new IntersectionObserver(function(entries) {
// If intersectionRatio is 0, the target is out of view
// and we do not need to do anything.
if (entries[0].intersectionRatio <= 0) return;

loadItems(10);
console.log('Loaded new items');
});
// start observing
intersectionObserver.observe(document.querySelector('.scrollerFooter'));

阮一峰老师有一篇文章写的很简洁易懂,推荐去看看。

微信小程序 wx.createIntersectionObserver

小程序的此API与web端的API如出一辙,使用方法大致相似。

使用步骤:

1、wx.createIntersectionObserver 创建并返回一个IntersectionObserver 对象实例。

2、指定可视窗口参照物

  • IntersectionObserver.relativeTo 传入一个父级元素,作为可视窗口
  • IntersectionObserver.relativeToViewport 指定页面的整个窗口作为可视参照

3、IntersectionObserver.observe 开始监听

使用步骤大概就是这三步,具体的配置可参考文档。

总结

研究哇这个api后,果断将判断吸顶时用的scroll时间替换为IntersectionObserver,体验就是写法上更简洁,性能更好,能满足更多需求。

所以基本上IntersectionObserver可以完爆scroll了,一些使用广泛的库也开始使用此方案了,so 赶快用起来吧。

Author

Ludis

Posted on

2020-05-14

Updated on

2020-05-14

Licensed under

Comments