IntersectionObserver API 使用教程

作者: 阮一峰

日期: 2016年11月 3日

网页开发时,常常需要了解某个元素是否进入了"视口"(viewport),即用户能不能看到它。

上图的绿色方块不断滚动,顶部会提示它的可见性。

传统的实现方法是,监听到scroll事件后,调用目标元素(绿色方块)的getBoundingClientRect()方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件密集发生,计算量很大,容易造成性能问题

目前有一个新的 IntersectionObserver API,可以自动"观察"元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个 API 叫做"交叉观察器"。

一、API

它的用法非常简单。


var io = new IntersectionObserver(callback, option);

上面代码中,IntersectionObserver是浏览器原生提供的构造函数,接受两个参数:callback是可见性变化时的回调函数,option是配置对象(该参数可选)。

构造函数的返回值是一个观察器实例。实例的observe方法可以指定观察哪个 DOM 节点。


// 开始观察
io.observe(document.getElementById('example'));

// 停止观察
io.unobserve(element);

// 关闭观察器
io.disconnect();

上面代码中,observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。


io.observe(elementA);
io.observe(elementB);

二、callback 参数

目标元素的可见性变化时,就会调用观察器的回调函数callback

callback一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。


var io = new IntersectionObserver(
  entries => {
    console.log(entries);
  }
);

上面代码中,回调函数采用的是箭头函数的写法。callback函数的参数(entries)是一个数组,每个成员都是一个IntersectionObserverEntry对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。

三、IntersectionObserverEntry 对象

IntersectionObserverEntry对象提供目标元素的信息,一共有六个属性。


{
  time: 3893.92,
  rootBounds: ClientRect {
    bottom: 920,
    height: 1024,
    left: 0,
    right: 1024,
    top: 0,
    width: 920
  },
  boundingClientRect: ClientRect {
     // ...
  },
  intersectionRect: ClientRect {
    // ...
  },
  intersectionRatio: 0.54,
  target: element
}

每个属性的含义如下。

  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • target:被观察的目标元素,是一个 DOM 节点对象
  • rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
  • boundingClientRect:目标元素的矩形区域的信息
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • intersectionRatio:目标元素的可见比例,即intersectionRectboundingClientRect的比例,完全可见时为1,完全不可见时小于等于0

上图中,灰色的水平方框代表视口,深红色的区域代表四个被观察的目标元素。它们各自的intersectionRatio图中都已经注明。

我写了一个 Demo,演示IntersectionObserverEntry对象。注意,这个 Demo 只能在 Chrome 51+ 运行。

四、实例:惰性加载(lazy load)

有时,我们希望某些静态资源(比如图片),只有用户向下滚动,它们进入视口时才加载,这样可以节省带宽,提高网页性能。这就叫做"惰性加载"。

有了 IntersectionObserver API,实现起来就很容易了。


function query(selector) {
  return Array.from(document.querySelectorAll(selector));
}

var observer = new IntersectionObserver(
  function(changes) {
    changes.forEach(function(change) {
      var container = change.target;
      var content = container.querySelector('template').content;
      container.appendChild(content);
      observer.unobserve(container);
    });
  }
);

query('.lazy-loaded').forEach(function (item) {
  observer.observe(item);
});

上面代码中,只有目标区域可见时,才会将模板内容插入真实 DOM,从而引发静态资源的加载。

五、实例:无限滚动

无限滚动(infinite scroll)的实现也很简单。


var intersectionObserver = new IntersectionObserver(
  function (entries) {
    // 如果不可见,就返回
    if (entries[0].intersectionRatio <= 0) return;
    loadItems(10);
    console.log('Loaded new items');
  });

// 开始观察
intersectionObserver.observe(
  document.querySelector('.scrollerFooter')
);

无限滚动时,最好在页面底部有一个页尾栏(又称sentinels)。一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的条目放在页尾栏前面。这样做的好处是,不需要再一次调用observe()方法,现有的IntersectionObserver可以保持使用。

六、Option 对象

IntersectionObserver构造函数的第二个参数是一个配置对象。它可以设置以下属性。

6.1 threshold 属性

threshold属性决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。


new IntersectionObserver(
  entries => {/* ... */}, 
  {
    threshold: [0, 0.25, 0.5, 0.75, 1]
  }
);

用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。

6.2 root 属性,rootMargin 属性

很多时候,目标元素不仅会随着窗口滚动,还会在容器里面滚动(比如在iframe窗口里滚动)。容器内滚动也会影响目标元素的可见性,参见本文开始时的那张示意图。

IntersectionObserver API 支持容器内滚动。root属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。


var opts = { 
  root: document.querySelector('.container'),
  rootMargin: "500px 0px" 
};

var observer = new IntersectionObserver(
  callback,
  opts
);

上面代码中,除了root属性,还有rootMargin属性。后者定义根元素的margin,用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。它使用CSS的定义方法,比如10px 20px 30px 40px,表示 top、right、bottom 和 left 四个方向的值。

这样设置以后,不管是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器。

七、注意点

IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。

规格写明,IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。

八、参考链接

(完)

留言(52条)

这个API很有用啊,不过会不会像 Object.observe 一样的命运

希望其他浏览器也早日实现

从caniuse来看支持程度还挺乐观,除了IE。Chrome从49就开始支持了
统计也是个最合适的应用场景

目前正在做相关的项目,今天有幸看到这篇文章,受益匪浅,谢谢阮哥长期以来的分享!

例子可以下载吗?

垫片:https://github.com/WICG/IntersectionObserver/blob/gh-pages/polyfill/intersection-observer.js

懒加载常用的是jquery插件jquery.lazyload.js,现在有了IntersectionObserver API,可以把它写进这个lazyload.js中更新了。看了下电脑上的chrome54,一会就可以测试下

感觉阮大神的分享。不错

新技术是第一生产力~

写的很好,,受教了~

长期以来,受您教育。十分感谢!

很有用的api!

“即只有线程空闲下来,才会执行观察器”。这会造成什么影响呢?

可以分享吧?

应该贴个收款二维码,这么好的资料。书店也买不到。

not bad! I will try it out ASAP!

喜欢阮老师的博客,收益很多,今天买了您的几本书看看

这个对象对不支持的浏览器有polyfill吗,有的话就能生产用了

本页面的下一页链接(JavaScript )的标题文字不完整。

阮老师最开始的博客是怎么坚持下来的,现在看最开始的文章,冒犯的说,确实有点水的。能从那样的初级水平坚持到现在读者满天下,阮老师的精神值得学习。

这个api在移动端的兼容性怎么样?特别是微信浏览器

引用Mason King的发言:

阮老师最开始的博客是怎么坚持下来的,现在看最开始的文章,冒犯的说,确实有点水的。能从那样的初级水平坚持到现在读者满天下,阮老师的精神值得学习。

谁不是从小白过来的呢

懒加载那部分代码应该有点问题。

因为元素一旦被observe,即io.observe(el),无论元素可不可见,
io的回调都会被调用一次,这样懒加载就没有意义了。

懒加载那里的回调应该加上判断,当元素不可见时不触发回调

var content = container.querySelector('template').content;
container.appendChild(content); 不太明白是什么意思,可以解释一下吗?

懒加载那一部分有问题, 初始化就把所有的东西都渲染了, 没有实现正真的懒加载

懒加载的哪里应该是这样的:
if (change.intersectionRatio > 0) {
var container = change.target;
var content = container.attributes('data-content');
container.appendChild(content);
observer.unobserve(container);
}

@toringo:

是的 应该加个判断,if(change.intersectionRatio > 0 && change.intersectionRatio

safari 目前还不支持, 所以想直接用的, 还是洗洗睡吧

引用yzg的发言:

safari 目前还不支持, 所以想直接用的, 还是洗洗睡吧

对的,最近使用lazyload,换成这种用法,发现safari不行。。。。。。。。。

WebKit 的 Intersection Observer 已经是 In Development 状态了。

https://webkit.org/status/#specification-intersection-observer

当观察的元素的定位值设置了负值(比如 left: -1px),而且给它加的动画是从该方向进来(比如 transform: translate(-100%, 0)),这时回调函数是一直触发的,建议判断 entry.intersectionRatio > 0 之后, 取消观察这个元素,延迟几毫之后再重新观察它。

IntersectionObserverEntry 对象目前又增加了一个 isIntersecting 属性,共 7 个了。https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserverEntry/isIntersecting

四、实例:惰性加载(lazy load)的代码有点问题,没有判断是否滚动到元素视口。

引用鸠摩智的发言:

四、实例:惰性加载(lazy load)的代码有点问题,没有判断是否滚动到元素视口。

在 container.appendChild(content);前面加上 if (change.intersectionRatio

这样是必须到最底部并且空闲时执行,要是想指定距离底部多少时执行的话,就需要body相对定位,然后参照元素绝对定位实现,弄个配置项,动态插入参照元素距底部多少,目前没发现其他好的方法,这样实现唯一的问题就是body需要相对定位,而且是必须

受教!写的真的太清晰了!

这个api只触发一次吗,多次出现屏幕内不触发

华为自带浏览器不支持这个api,求大神分享解决方法

引用老槐树的发言:

华为自带浏览器不支持这个api,求大神分享解决方法

官方有pollify

我就喜欢看阮大神的文章,通俗易懂,直击要点。

这个api的优先级过低了,我封装成图片懒加载组件,用来展示一个大量图片的列表时,会出现一个现象:图片是几张几张地加载。我猜测是因为部分图片触发了请求图片,导致别的本应也触发了的图片,要等待前面图片加载出来才开始请求图片。这个有大佬分享一下解决方案吗?

毕竟阮大神,写的教程很强啊

callback不是只有消失和出现才会触发吧,应该是每次出现的区域比例发生变动就会触发吧

老师,sentinels 链接已经失效了

引用sjh的发言:

callback不是只有消失和出现才会触发吧,应该是每次出现的区域比例发生变动就会触发吧

变动不会触发,只有出现或消失才会触发

在发现这个API之前,我还在计算元素到屏幕和可见区域的距离来搞这些。o(╥﹏╥)o

规格写明,IntersectionObserver的实现,应该采用requestIdleCallback()

可以问下这句话在规范里有写到吗,我找了一圈,没看到

引用Dedicatus545的发言:

规格写明,IntersectionObserver的实现,应该采用requestIdleCallback()

可以问下这句话在规范里有写到吗,我找了一圈,没看到

MDN上有说,回调函数会在主线程中执行,如果有耗时的操作,建议使用requestIdleCallback()
https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API

2022年还在看阮老师2016年的教程????

简单明了

引用wenlan的发言:

2022年还在看阮老师2016年的教程????

2023年了还在看

引用张传峰的发言:

IntersectionObserverEntry 对象目前又增加了一个 isIntersecting 属性,共 7 个了。https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserverEntry/isIntersecting

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接