网页开发时,常常需要了解某个元素是否进入了"视口"(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
:目标元素的可见比例,即intersectionRect
占boundingClientRect
的比例,完全可见时为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()
,即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。
八、参考链接
(完)
Jack 说:
这个API很有用啊,不过会不会像 Object.observe 一样的命运
2016年11月 3日 08:00 | # | 引用
jon_s 说:
希望其他浏览器也早日实现
2016年11月 3日 11:07 | # | 引用
Robin 说:
从caniuse来看支持程度还挺乐观,除了IE。Chrome从49就开始支持了
统计也是个最合适的应用场景
2016年11月 3日 14:13 | # | 引用
许景峰 说:
目前正在做相关的项目,今天有幸看到这篇文章,受益匪浅,谢谢阮哥长期以来的分享!
2016年11月 3日 19:15 | # | 引用
feng 说:
例子可以下载吗?
2016年11月 4日 10:30 | # | 引用
honpery 说:
垫片:https://github.com/WICG/IntersectionObserver/blob/gh-pages/polyfill/intersection-observer.js
2016年11月 4日 12:05 | # | 引用
wilsonIs 说:
懒加载常用的是jquery插件jquery.lazyload.js,现在有了IntersectionObserver API,可以把它写进这个lazyload.js中更新了。看了下电脑上的chrome54,一会就可以测试下
2016年11月 5日 09:35 | # | 引用
waerly 说:
感觉阮大神的分享。不错
2016年11月 5日 15:25 | # | 引用
轩辕Rowboat 说:
新技术是第一生产力~
2016年11月 6日 00:02 | # | 引用
FiShelly. 说:
写的很好,,受教了~
2016年11月 6日 17:14 | # | 引用
meihuan 说:
长期以来,受您教育。十分感谢!
2016年11月 6日 20:15 | # | 引用
山河 说:
很有用的api!
2016年11月 8日 09:47 | # | 引用
动感小前端 说:
“即只有线程空闲下来,才会执行观察器”。这会造成什么影响呢?
2016年11月 8日 10:00 | # | 引用
themebetter 说:
可以分享吧?
2016年11月18日 10:16 | # | 引用
captain 说:
应该贴个收款二维码,这么好的资料。书店也买不到。
2016年11月18日 12:58 | # | 引用
xgqfrms 说:
not bad! I will try it out ASAP!
2016年11月19日 00:08 | # | 引用
lhp9916 说:
喜欢阮老师的博客,收益很多,今天买了您的几本书看看
2016年11月19日 14:30 | # | 引用
syperwf 说:
这个对象对不支持的浏览器有polyfill吗,有的话就能生产用了
2016年12月20日 17:59 | # | 引用
瓘木 说:
本页面的下一页链接(JavaScript )的标题文字不完整。
2017年2月16日 12:15 | # | 引用
Mason King 说:
阮老师最开始的博客是怎么坚持下来的,现在看最开始的文章,冒犯的说,确实有点水的。能从那样的初级水平坚持到现在读者满天下,阮老师的精神值得学习。
2017年2月27日 20:08 | # | 引用
tobyforever 说:
这个api在移动端的兼容性怎么样?特别是微信浏览器
2017年3月10日 17:19 | # | 引用
Amor 说:
谁不是从小白过来的呢
2017年3月15日 13:16 | # | 引用
jessty 说:
懒加载那部分代码应该有点问题。
因为元素一旦被observe,即io.observe(el),无论元素可不可见,
io的回调都会被调用一次,这样懒加载就没有意义了。
懒加载那里的回调应该加上判断,当元素不可见时不触发回调
2017年3月15日 20:14 | # | 引用
leafront 说:
var content = container.querySelector('template').content;
container.appendChild(content); 不太明白是什么意思,可以解释一下吗?
2017年7月17日 11:21 | # | 引用
糖小米 说:
懒加载那一部分有问题, 初始化就把所有的东西都渲染了, 没有实现正真的懒加载
2017年8月 8日 15:07 | # | 引用
toringo 说:
懒加载的哪里应该是这样的:
if (change.intersectionRatio > 0) {
var container = change.target;
var content = container.attributes('data-content');
container.appendChild(content);
observer.unobserve(container);
}
2017年8月24日 18:27 | # | 引用
vic 说:
@toringo:
是的 应该加个判断,if(change.intersectionRatio > 0 && change.intersectionRatio
2017年12月27日 16:24 | # | 引用
yzg 说:
safari 目前还不支持, 所以想直接用的, 还是洗洗睡吧
2018年2月13日 16:03 | # | 引用
小小龙 说:
对的,最近使用lazyload,换成这种用法,发现safari不行。。。。。。。。。
2018年4月 1日 11:28 | # | 引用
xxx 说:
WebKit 的 Intersection Observer 已经是 In Development 状态了。
https://webkit.org/status/#specification-intersection-observer
2018年4月18日 18:24 | # | 引用
jerry 说:
当观察的元素的定位值设置了负值(比如 left: -1px),而且给它加的动画是从该方向进来(比如 transform: translate(-100%, 0)),这时回调函数是一直触发的,建议判断 entry.intersectionRatio > 0 之后, 取消观察这个元素,延迟几毫之后再重新观察它。
2018年12月22日 10:34 | # | 引用
张传峰 说:
IntersectionObserverEntry 对象目前又增加了一个 isIntersecting 属性,共 7 个了。https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserverEntry/isIntersecting
2018年12月27日 11:22 | # | 引用
鸠摩智 说:
四、实例:惰性加载(lazy load)的代码有点问题,没有判断是否滚动到元素视口。
2019年1月 9日 11:36 | # | 引用
鸠摩智 说:
2019年1月 9日 11:40 | # | 引用
陈子义 说:
这样是必须到最底部并且空闲时执行,要是想指定距离底部多少时执行的话,就需要body相对定位,然后参照元素绝对定位实现,弄个配置项,动态插入参照元素距底部多少,目前没发现其他好的方法,这样实现唯一的问题就是body需要相对定位,而且是必须
2019年12月12日 14:39 | # | 引用
西门吹雪 说:
受教!写的真的太清晰了!
2020年10月23日 00:12 | # | 引用
wangchong 说:
这个api只触发一次吗,多次出现屏幕内不触发
2020年12月10日 17:51 | # | 引用
老槐树 说:
华为自带浏览器不支持这个api,求大神分享解决方法
2021年2月26日 17:12 | # | 引用
葱头 说:
官方有pollify
2021年3月30日 17:20 | # | 引用
杨刚 说:
我就喜欢看阮大神的文章,通俗易懂,直击要点。
2021年6月29日 17:19 | # | 引用
janka55 说:
这个api的优先级过低了,我封装成图片懒加载组件,用来展示一个大量图片的列表时,会出现一个现象:图片是几张几张地加载。我猜测是因为部分图片触发了请求图片,导致别的本应也触发了的图片,要等待前面图片加载出来才开始请求图片。这个有大佬分享一下解决方案吗?
2021年8月14日 15:02 | # | 引用
chuck testa 说:
毕竟阮大神,写的教程很强啊
2021年9月 5日 01:45 | # | 引用
sjh 说:
callback不是只有消失和出现才会触发吧,应该是每次出现的区域比例发生变动就会触发吧
2021年9月27日 17:58 | # | 引用
zhuyunfeng 说:
老师,sentinels 链接已经失效了
2021年11月18日 14:34 | # | 引用
zhuyunfeng 说:
变动不会触发,只有出现或消失才会触发
2021年11月18日 14:35 | # | 引用
cxh 说:
在发现这个API之前,我还在计算元素到屏幕和可见区域的距离来搞这些。o(╥﹏╥)o
2021年12月 1日 10:48 | # | 引用
Dedicatus545 说:
规格写明,IntersectionObserver的实现,应该采用requestIdleCallback()
可以问下这句话在规范里有写到吗,我找了一圈,没看到
2022年3月 9日 14:23 | # | 引用
Wing 说:
MDN上有说,回调函数会在主线程中执行,如果有耗时的操作,建议使用requestIdleCallback()
https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API
2022年3月15日 11:56 | # | 引用
wenlan 说:
2022年还在看阮老师2016年的教程????
2022年9月 2日 12:05 | # | 引用
jc 说:
简单明了
2023年1月 9日 21:11 | # | 引用
二货快跑 说:
2023年了还在看
2023年6月 8日 10:37 | # | 引用