- IntersectionObserver概览
- IntersectionObserver构造器
- IntersectionObserver方法
- IntersectionObserver懒加载(vue单文件组件简版)
- IntersectionObserver吸顶(vue单文件组件简版)
- IntersectionObserver触底(vue单文件组件简版)
- IntersectionObserver懒加载、吸顶、触底综合(vue单文件组件实战版)
- 基于Intersection的开源工具 Scrollama
- 总结
- 参考资料
IntersectionObserver概览
- IntersectionObserver提供了一个方式去异步观察 有一个祖先element或者top-level document viewport的目标element 的交叉变化。
- 祖先元素或者viewport被当做root节点。
- 当IntersectionObserver创建出的时候,它会被配置到监听root内部的给定visibility的变化。
- 一旦IntersectionObserver创建出来,它的配置是不能变的。 所以一个observer object只能用来监测一个指定visibility值的变化。
- 虽然只能一对一去watch ratio,但是可以在同一个observer中watch多个target elements。也就是说一个visibility ratio可以检测多个不同的elements。
IntersectionObserver构造器
var observer = new IntersectionObserver(callback[, options]);
- 和其他构造器一样,创建并返回一个新的Intersection对象。
- rootMargin如果指定一个特殊值,是为了确保语法是否正确。
- 阀值是为了确保值在0.0到1.0之间,threshold会按照升序排列。若threshold是空,值为[0.0]。
参数
- callback 当目标元素的透明度穿过设定的threshold值时,函数会被调用。callback接受两个 参数。
- entries 传入各个threshold值的数组,比该阀值指定的百分比更明显或者更不明显。
- observer 调用callback的observer实例。
- options 若options没设置。observer使用document的viewport作为root,没有margin,0%的threshold(意味着即使有1 px的变化也会触发回调)
- root 被当做viewport的元素。
- rootMargin 语法是"0px 0px 0px 0px"
- threshold 指明被监测目标总绑定盒模型的交叉区域ratio,值在0.0到1.0之间;0.0意味着即使是1px也会被当做可见的。1.0意味着整个元素是可见的。默认threshold值是0.0。
IntersectionObserver方法
- IntersectionObserver.disconnect() 停止observe一个目标。
- IntersectionObserver.observe() 告诉IntersectionObserver一目标元素去observe。
- IntersectionObserver.takeRecords() 返回包含所有observe的对象一个数组。
- IntersectionObserver.unobserve() 取消observe一个目标对象。
示例
下面的例子在threshold值变化在10%以上时触发myObserverCallback。
let observer = new IntersectionObserver(myObserverCallback, { "threshold": 0.1 });
IntersectionObserver懒加载(vue单文件组件简版)
<template> <div> <img v-for="(image, i) in images" :key="i" src :data-img-url="image" /> </div> </template> <script> export default { data() { return { images: [ 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247890587&di=88d4066be3d57ac962a6bec37e265d37&imgtype=0&src=http%3A%2F%2F01.imgmini.eastday.com%2Fmobile%2F20170810%2F20170810151144_d41d8cd98f00b204e9800998ecf8427e_3.jpeg', 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4054762707,1853885380&fm=26&gp=0.jpg', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247912077&di=508a949e5e291875debf6ca844292cd4&imgtype=0&src=http%3A%2F%2F03imgmini.eastday.com%2Fmobile%2F20180827%2F20180827095359_6759372e9bd28026ee6f53b500fb4291_2.jpeg', ], }; }, mounted() { const images = document.querySelectorAll('img'); const observerLazyLoad = new IntersectionObserver((entries) => { entries.forEach((item) => { if (item.isIntersecting) { item.target.src = item.target.dataset.imgUrl; } }); }); images.forEach((image) => { observerLazyLoad.observe(image); }); }, }; </script> <style lang="scss" scoped> img { display: block; height: 500px; margin: 30px; } </style>
IntersectionObserver吸顶(vue单文件组件简版)
<template> <div> <p class="fixed-top-helper"></p> <p class="fixed-top-reference"></p> <header>头部</header> <main> <img v-for="(image, i) in images" :key="i" :src="image" /> </main> </div> </template> <script> export default { data() { return { images: [ 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247890587&di=88d4066be3d57ac962a6bec37e265d37&imgtype=0&src=http%3A%2F%2F01.imgmini.eastday.com%2Fmobile%2F20170810%2F20170810151144_d41d8cd98f00b204e9800998ecf8427e_3.jpeg', 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4054762707,1853885380&fm=26&gp=0.jpg', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247912077&di=508a949e5e291875debf6ca844292cd4&imgtype=0&src=http%3A%2F%2F03imgmini.eastday.com%2Fmobile%2F20180827%2F20180827095359_6759372e9bd28026ee6f53b500fb4291_2.jpeg', ], }; }, mounted() { const header = document.querySelector('header'); const fixedTopReference = document.querySelector('.fixed-top-reference'); fixedTopReference.style.top = `${header.offsetTop}px`; const observerFixedTop = new IntersectionObserver((entries) => { entries.forEach((item) => { if (item.boundingClientRect.top < 0) { header.classList.add('fixed'); } else { header.classList.remove('fixed'); } }); }); observerFixedTop.observe(fixedTopReference); }, }; </script> <style lang="scss" scoped> .fixed-top-helper { height: 1px; background: #ccc; } header { background: #ccc; &.fixed { position: fixed; top: 0; left: 0; width: 100%; } } main { img { display: block; height: 500px; margin: 30px; } } </style>
注意事项:
- fixedTopReference是为了避免缓慢移动时add remove .fixed死循环,死循环的结果是抖动
- fixedTopHelper是为了避免被吸顶元素没有上一个sibling元素(也就是说被吸顶元素是最上层元素)时,避免缓缓移动时add remove .fixed死循环抖动,特殊引入的标签,需要设置1个px的height
- fixedTopHelper需要与被吸顶元素保持样式一致,以确保好的用户体验。例如在本例中将其background设置为#ccc,很好的做到了隐藏
吸顶抖动
IntersectionObserver触底(vue单文件组件简版)
<template> <div> <main> <img v-for="(image, i) in images" :key="i" src="image" /> </main> <footer>底部</footer> </div> </template> <script> export default { data() { return { images: [ 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247890587&di=88d4066be3d57ac962a6bec37e265d37&imgtype=0&src=http%3A%2F%2F01.imgmini.eastday.com%2Fmobile%2F20170810%2F20170810151144_d41d8cd98f00b204e9800998ecf8427e_3.jpeg', 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4054762707,1853885380&fm=26&gp=0.jpg', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247912077&di=508a949e5e291875debf6ca844292cd4&imgtype=0&src=http%3A%2F%2F03imgmini.eastday.com%2Fmobile%2F20180827%2F20180827095359_6759372e9bd28026ee6f53b500fb4291_2.jpeg', ], }; }, mounted() { const footer = document.querySelector('footer'); const observerTouchBottom = new IntersectionObserver((entries) => { entries.forEach((item) => { if (item.isIntersecting) { setTimeout(() => { console.log('滚动到了底部,可以发request请求数据了'); }, 2000); } }); }); observerTouchBottom.observe(footer); }, }; </script> <style lang="scss" scoped> main { img { display: block; height: 500px; margin: 30px; } } footer { background: #ccc; } </style>
IntersectionObserver懒加载、吸顶、触底综合(vue单文件组件实战版)
上面的例子是为了脱离框架更好的揭示IntersectionObserver的用法本质,如果在实际项目中使用,还需要考虑一些其他问题。
考虑内容如下:
- 对象拆分,下面拆分出lazyLoad,touchFooter,stickHeader三个对象并新建target和observer来分别标识被监听者和监听者
- 方法拆分,摒弃全部在mounted方法中变量的定义和赋值操作,很清晰的拆分出createLazyLoadObserver,createTouchFooterObserver,createStickHeaderObserver三个方法
- 取消监听,新建unobserveAllIntersectionObservers方法,在beforeDestory生命周期内,调用IntersectionObserver的disconnect(),unbserve(target)取消监听目标对象
<template> <div> <p class="fixed-top-helper"></p> <p class="fixed-top-reference"></p> <header>头部</header> <main> <img v-for="(image, i) in images" :key="i" src :data-img-url="image" /> </main> <footer>底部</footer> </div> </template> <script> const images = [ 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247890587&di=88d4066be3d57ac962a6bec37e265d37&imgtype=0&src=http%3A%2F%2F01.imgmini.eastday.com%2Fmobile%2F20170810%2F20170810151144_d41d8cd98f00b204e9800998ecf8427e_3.jpeg', 'https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=4054762707,1853885380&fm=26&gp=0.jpg', 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1574247912077&di=508a949e5e291875debf6ca844292cd4&imgtype=0&src=http%3A%2F%2F03imgmini.eastday.com%2Fmobile%2F20180827%2F20180827095359_6759372e9bd28026ee6f53b500fb4291_2.jpeg', ]; export default { data() { return { images, lazyLoad: { target: null, observer: null, }, touchFooter: { target: null, observer: null, }, stickHeader: { target: null, reference: null, observer: null, }, }; }, mounted() { this.createLazyLoadObserver(); this.createTouchFooterObserver(); this.createStickHeaderObserver(); }, beforeDestroy() { this.unobserveAllIntersectionObservers(); }, methods: { /* * 创建懒加载observer并遍历监听所有img */ createLazyLoadObserver() { this.lazyLoad.target = document.querySelectorAll('img'); this.lazyLoad.observer = new IntersectionObserver((entries) => { entries.forEach((item) => { if (item.isIntersecting) { item.target.src = item.target.dataset.imgUrl; } }); }); this.lazyLoad.target.forEach((image) => { this.lazyLoad.observer.observe(image); }); }, /* * 创建触底observer并监听footer */ createTouchFooterObserver() { this.touchFooter.target = document.querySelector('footer'); this.touchFooter.observer = new IntersectionObserver((entries) => { entries.forEach((item) => { if (item.isIntersecting) { setTimeout(() => { console.log('滚动到了底部,可以发request请求数据了'); }, 2000); } }); }); this.touchFooter.observer.observe(this.touchFooter.target); }, /* * 创建吸顶observer并监听header * 创建reference首次防抖,.fixed-top-helper二次防抖 */ createStickHeaderObserver() { this.stickHeader.target = document.querySelector('header'); this.stickHeader.reference = document.querySelector('.fixed-top-reference'); this.stickHeader.reference.style.top = `${this.stickHeader.target.offsetTop}px`; this.stickHeader.observer = new IntersectionObserver((entries) => { entries.forEach((item) => { if (item.boundingClientRect.top < 0) { this.stickHeader.target.classList.add('fixed'); } else { this.stickHeader.target.classList.remove('fixed'); } }); }); this.stickHeader.observer.observe(this.stickHeader.reference); }, /* * 取消observe所有监听目标 */ unobserveAllIntersectionObservers() { /* * disconncet()可以取消所有observed目标 * 如果调用unobserve取消监听,稍显冗余的代码如下: this.lazyLoad.target.forEach((image) => { this.lazyLoad.observer.unobserve(image); }); */ this.lazyLoad.observer.disconnect(); /* * 由于touchFooter和stickHeader只observe了一个目标,因此单独unobserve即可 * 当然disconnect()也是ok的 */ this.touchFooter.observer.unobserve(this.touchFooter.target); this.stickHeader.observer.unobserve(this.stickHeader.reference); }, }, }; </script> <style lang="scss" scoped> .fixed-top-helper { height: 1px; background: #ccc; } header { background: #ccc; &.fixed { position: fixed; top: 0; left: 0; width: 100%; } } main { img { display: block; height: 500px; margin: 30px; } } footer { background: #ccc; } </style>
基于Intersection的开源工具 Scrollama
官方提供了Basic Process,Progress,Sticky Side,Sticky Overlay几种示例。
Basic Process
Progress
Sticky Overlay
在项目中可以当做适当引用。
项目地址:https://github.com/russellgol...
demo地址:https://russellgoldenberg.git...
总结
- 主要使用IntersectionObserver实现懒加载图片,触底,吸顶
- vue单文件组件简版主要是用于演示,vue单文件组件实战版可用于项目
- 虽然我这里演示的是vue单文件组件的版本,但是我相信聪明的你知道核心部分在哪里
- IntersectionObserver可能还会有其他的用处,来日方长,慢慢探索
参考资料
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...
https://juejin.im/post/5ca15c...
https://medium.com/walmartlab...
https://juejin.im/post/5d6651...