利用"交叉观察者"(IntersectionObserver接口),轻松实现懒加载、吸顶、触底
0 条评论原文地址:https://juejin.im/post/5d665133e51d4561c83e7c83#heading-1
可以先看一下MDN中的介绍:IntersectionObserver接口,提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法,祖先元素与视窗(viewport)被称为根(root);
直接进入正题,IntersectionObserver 翻译为
“交叉观察者“,它的任务就是监听目标元素跟指定父元素(用户可指定,默认为viewport)是否在发生交叉行为,简单理解就是监听目标元素是否进入或者离开了指定父元素的内部(理解这句就行了,管他交不交叉呢),我好像在开车,但是你们没有证据
… 😐
以下的目标元素简称为目标、指定父元素简称为父亲、交叉行为简称为交叉,viewport简称为视窗 👌
下面会有动图介绍,先忍忍!
用法
1. 构造函数
new IntersectionObserver(callback, options);
2. callback
发生交叉的回调,接受一个entries参数,返回当前已监听并且发生了交叉的目标集合(后面会举例说明为什么是”且发生了交叉“):
new IntersectionObserver(entries => {
entries.forEach(item => console.log(item));
// ...
});
我们看看item里面包含哪些常用属性:
属性说明boundingClientRect空间信息intersectionRatio元素可见区域的占比isIntersecting字面理解为是否正在交叉,可用做判断元素是否可见target目标节点,就跟event.target一样
注意:页面初始化的时候会触发一次callback,entries为所有已监听的目标集合✅
3. options
顾名思义,它是一个配置参数,对象类型,非必填,常用属性如下:
属性说明root指定父元素,默认为视窗rootMargin触发交叉的偏移值,默认为”0px 0px 0px
0px”(上左下右,正数为向外扩散,负数则向内收缩)
new IntersectionObserver(callback, {
root: document.querySelector(“xx”),
rootMargin: “0px 0px -100px 0px”
});
如果设置rootMargin为”20px 0px 30px 30px“,那么元素未到达视窗时,就已经切换为可见状态了:
4. 常用方法
名称说明参数observe开始监听一个目标元素节点unobserve停止监听一个目标元素节点takeRecords返回所有监听的目标元素集合disconnect停止所有监听
例子
1. 假设页面上有一个class="box"的盒子且父元素为视窗:
let box = document.querySelector(".box");
let observer = new IntersectionObserver(entries => {
entries.forEach(item => {
let tips = item.isIntersecting ? "进入了父元素的内部" : "离开了父元素的内部";
console.log(tips);
});
});
observer.observe(box); // 监听一个box
效果如下:
2. 假设页面上有多个class="box"的盒子且父元素为视窗:
let box = document.querySelectorAll(".box");
let observer = new IntersectionObserver(entries => console.log(`发生交叉行为,目标元素有${entries.length}个`));
box.forEach(item => observer.observe(item)); // 监听多个box
当所有盒子距离视窗顶部距离一致时,效果如下:
当所有盒子距离视窗顶部距离不一致时,效果如下:
为什么要举例以上两种情况呢,因为entries是返回当前已监听并且发生了交叉的目标集合,第一种情况,大家都一起发生交叉,固每次返回的集合长度都为三;第二种情况则是每个目标轮流发生交叉,且当前只触发了一个,所以每次返回的集合长度只有一✅
3. 指定父元素
假设html如下:
<div class="parent">
<div class="child"></div>
</div>
然后开始监听:
let child = document.querySelector(".child");
let observer = new IntersectionObserver(entries => {
entries.forEach(item => {
console.log(item.isIntersecting ? "可见" : "不可见");
});
}, {
root: document.querySelector(".parent")
});
observer.observe(child); // 开始监听child
效果如下:
实际应用
1. 图片懒加载
以前都是监听浏览器滚动,然后遍历拿到每个图片的空间信息,然后判断一些位置信息从而进行图片加载;而现在只需要交给交叉观察者去做;
假设html结构如下:
// 多个
<img src="" data-origin="图片链接">
然后开始监听:
let images = document.querySelectorAll("img.lazyload");
let observer = new IntersectionObserver(entries => {
entries.forEach(item => {
if (item.isIntersecting) {
item.target.src = item.target.dataset.origin; // 开始加载图片
observer.unobserve(item.target); // 停止监听已开始加载的图片
}
});
});
images.forEach(item => observer.observe(item));
效果如下:
把网速调慢:
设置rootMargin偏移值为”0px 0px -100px 0px“(底部向内收缩):
该方法还有一个好处,那就是当页面上某个节点存在横向滚动条的时候,一样应对自如:
传统的懒加载只是监听全局滚动条的滚动,像这种小细节还是无法实现的(传统的实现方法并不是判断目标是否出现在视窗,所以横向的图片会一起加载,即使你没有向左滑动),所以这也是交叉观察者的一大优点✅
2. 触底
我们在列表底部放一个参照元素,然后让交叉观察者去监听;
假设html结构如下:
<!-- 数据列表 -->
<ul>
<li>index</li>
</ul>
<!-- 参照元素 -->
<div class="reference"></div>
然后监听参照元素:
new IntersectionObserver(entries => {
let item = entries[0]; // 拿第一个就行,反正只有一个
if (item.isIntersecting) console.log("滚动到了底部,开始请求数据");
}).observe(document.querySelector(".reference")); // 监听参照元素
效果如下:
3. 吸顶
实现元素吸顶的方式有很多种,如css的position: sticky,兼容性较差;如果用交叉观察者实现也很方便,同样也要放一个参照元素;
假设html结构如下:
<!-- 参照元素 -->
<div class="reference"></div>
<nav>我可以吸顶</nav>
假设scss代码如下:
nav {
&.fixed {
position: fixed;
top: 0;
left: 0;
width: 100%;
}
}
开始监听:
let nav = document.querySelector('nav');
let reference = document.querySelector(".reference");
new IntersectionObserver(entries => {
let item = entries[0];
let top = item.boundingClientRect.top;
// 当参照元素的的top值小于0,也就是在视窗的顶部的时候,开始吸顶,否则移除吸顶
if (top < 0) nav.classList.add("fixed");
else nav.classList.remove("fixed");
}).observe(reference);
效果如下:
但是有个问题,当你滚动的慢的时候,会掉进一个死循环:
为了方便观察,我们把参考元素加一个高度跟颜色:
问题很明显,当给nav增加fixed定位时,nav脱离了文档流,自然参考元素会往下掉,然后往下掉又发生了交叉,从而去除fixed定位,陷入一个死循环;
思考了一会,解决办法是,让参考元素绝对定位至nav的上方:
let nav = document.querySelector('nav');
let reference = document.querySelector(".reference");
reference.style.top = nav.offsetTop + "px";
// 以下代码不变 ...
这样,即使nav脱离的文档流,也不会影响参考元素的位置:
4. 动画展示
相信很多人都需要过这种需求,当某个元素出现的时候就给该元素加个动画,比如渐变、偏移等;
假设html结构如下:
<ul>
<li></li>
</ul>
假设scss代码如下:
ul {
li {
&.show {
// 默认从左边进来
animation: left 1s ease;
// 偶数从右边进来
&:nth-child(2n) {
animation: right 1s ease;
}
}
}
}
@keyframes left {
from {
opacity: 0;
transform: translate(-20px, 20px); // right动画改成20px, 20px即可
}
to {
opacity: 1;
}
}
然后开始监听:
let list = document.querySelectorAll("ul li");
let observer = new IntersectionObserver(entries => {
entries.forEach(item => {
if (item.isIntersecting) {
item.target.classList.add("show"); // 增加show类名
observer.unobserve(item.target); // 移除监听
}
});
});
list.forEach(item => observer.observe(item));
效果如下:
兼容性
IE不兼容,不过有官方的polyfill✅
最后
暂时就发现这么多用途啦,值得注意的是,必须是子元素跟父元素发生交叉,如果你想检查两个非父子关系的交叉,那是不行的嘻嘻,如果你觉得这篇文章不错,请别忘记点个赞跟关注哦~😊
- 本文链接:https://xuehuayu.cn/article/a188a3ec.html
- 版权声明:① 标为原创的文章为博主原创,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接。② 部分文章内容由 AI 生成,内容仅供参考,请仔细甄别。③ 标为转载的文章来自网络,已标明出处,侵删。

