
sunilwang
2022/11/22阅读:29主题:橙心
说说IntersectionObserver交叉观察器
前言
过去,要检测一个元素是否可见或者两个元素是否相交并不容易,很多解决办法不可靠或性能很差。然而,随着互联网的发展,这种需求却与日俱增,比如,下面这些情况都需要用到相交检测:
图片懒加载——当图片滚动到可见时才进行加载 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况 在用户看见某个区域时执行任务或播放动画 过去,相交检测通常要用到事件监听,并且需要频繁调用 `Element.getBoundingClientRect()`[1] 方法以获取相关元素的边界信息。事件监听和调用 `Element.getBoundingClientRect()`[2] 都是在主线程上运行,因此频繁触发、调用可能会造成性能问题。这种检测方法极其怪异且不优雅。
Intersection Observer API 会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时 (或者 viewport[3] ),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。这样,我们网站的主线程不需要再为了监听元素相交而辛苦劳作,浏览器会自行优化元素相交管理。
用法
1. 介绍
Intersection Observer API 提供了一种异步检测目标元素与祖先元素或 viewport[4] 相交情况变化的方法
let io = new IntersectionObserver(callback, options);
上面代码中,IntersectionObserver
是浏览器原生提供的构造函数,接受两个参数:callback
是可见性变化时的回调函数,option
是配置对象(该参数可选)。
构造函数的返回值是一个观察器实例。实例的observe
方法可以指定观察哪个 DOM 节点。
2. 方法
方法 | 说明 |
---|---|
observe() | 开始监听一个目标元素 |
unobserve() | 停止监听特定目标元素 |
takeRecords() | 返回所有观察目标的 IntersectionObserverEntry 对象数组 |
disconnect() | 使 IntersectionObserver 对象停止全部监听工作 |
root | 用来获取当前 intersectionObserver 实例的根元素(只读) |
rootMargin | 与 CSS 属性`margin`[5]语法相似的字符串 (string) 对象。在交叉检测开始之前,由rootMargin 规定的矩形的每一边都会被添加至 `root`[6]元素的边框盒 ![]() |
thresholds | 一个包含阈值的列表,按升序排列,列表中的每个阈值都是监听对象的交叉区域与边界区域的比率(只读) |
// 开始观察
io.observe(document.getElementById('example'));
// 停止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();
上面代码中,observe
的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。
io.observe(elementA);
io.observe(elementB);
3. callback 参数
callback 以下情况会被调用:
-
每当目标 (target) 元素与设备视窗或者其他指定元素发生交集的时候执行 -
Observer 第一次监听目标元素的时候
接收一个参数 entries,即 IntersectionObserverEntry 实例。描述了目标元素与 root 的交叉状态。具体参数如下:
属性 | 说明 |
---|---|
boundingClientRect | 返回包含目标元素的边界信息,返回结果与 element.getBoundingClientRect() 相同![]() |
intersectionRatio | 返回目标元素出现在可视区的比例 |
intersectionRect | 用来描述 root 和目标元素的相交区域 |
isIntersecting | 返回一个布尔值,下列两种操作均会触发 callback:1. 过渡从不相交到相交,返回 true。2. 过渡从相交到不相交,返回 false |
rootBounds | 用来描述交叉区域观察者(intersection observer)中的根. |
target | 目标元素:与根出现相交区域改变的元素 (Element) |
time | 表示相交更改发生的时间相对于文档创建的时间,单位为毫秒 |
isVisible | 布尔值,目标元素是否可见(该属性还在试验阶段,不建议在生产环境中使用) |
4. options 参数
options 是一个对象,控制观察者的回调函数的被调用时的环境,也可以不填。共有三个属性,具体如下:
属性 | 说明 |
---|---|
root | 指定根 (root) 元素,用于检查目标的可见性。必须是目标元素的父级元素。如果未指定或者为null ,则默认为浏览器视窗 |
rootMargin | 根 (root) 元素的外边距。类似于 CSS 中的 `margin`[7] 属性,比如 "10px 20px 30px 40px" (top, right, bottom, left)。如果有指定 root 参数,则 rootMargin 也可以使用百分比来取值。该属性值是用作 root 元素和 target 发生交集时候的计算交集的区域范围,使用该属性可以控制 root 元素每一边的收缩或者扩张。默认值为 0。 |
threshold | 可以是单一的 number 也可以是 number 数组,target 元素和 root 元素相交程度达到该值的时候 IntersectionObserver 注册的回调函数将会被执行。默认值是 0 (意味着只要有一个 target 像素出现在 root 元素中,回调函数将会被执行) |
5. 简单示例
代码如下:
<template>
<div class="home">
<div
:class="['box', index === 3 ? 'bgc' : '']"
v-for="(item, index) in 10"
:key="index"
></div>
</div>
</template>
<script>
export default {
name: "HomeView",
mounted() {
// 获取要观察的DOM元素
const domList = document.querySelectorAll(".bgc");
this.change(domList);
},
methods: {
change(domList) {
// 获取观察器实例
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
console.log("============entry============");
console.log(entry);
});
}
);
domList.forEach((item) => {
// 开始观察
observer.observe(item);
});
},
},
};
</script>
<style scoped>
.box {
height: 300px;
}
.bgc {
background: #83f3bd;
}
.location1,
.location2 {
position: fixed;
right: 0;
color: #fff;
background-color: #2c3e50;
padding: 12px 30px;
border-radius: 4px 0 0 4px;
}
.location1 {
top: 80px;
}
.location2 {
top: 130px;
}
</style>
不设置 options 默认为浏览器视窗,结果如下图:

应用
1. 图片懒加载
<template>
<div class="lazy-load">
<div class="item" v-for="(item, index) in 10" :key="index">
<img
class="lazy-img"
:src="
index === 4
? 'https://pic5.58cdn.com.cn/union/n_v2ceedf7efa4df45e6b3ff6deb66dd1016_2f9594bb291b4132.jpg'
: ''
"
alt=""
:lazy="
index === 5
? 'https://pic7.58cdn.com.cn/union/n_v20387b69d577342b0865f016ab97f0ef7_1d0ae5d9dfc01f60.png'
: ''
"
/>
</div>
</div>
</template>
<script>
export default {
name: "LazyLoad",
data() {
return {};
},
mounted() {
const imgDomList = document.querySelectorAll(".lazy-img");
this.lazyLoad(imgDomList);
},
methods: {
// 图片懒加载
lazyLoad(imgDomList, preHeight = -140) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
// 过渡从不相交到相交
if (entry.isIntersecting) {
const target = entry.target;
const lazySrc = target.getAttribute("lazy");
const src = target.getAttribute("src");
if (!src && lazySrc) {
console.log("图片懒加载");
target.setAttribute("src", lazySrc);
}
observer.unobserve(target);
}
});
},
{
// 根元素面积向上缩小140px
rootMargin: `0px 0px ${preHeight}px 0px`,
}
);
imgDomList.forEach((item) => {
observer.observe(item);
});
},
},
};
</script>
<style scoped>
.item {
height: 202px;
}
.lazy-img {
width: 100%;
}
</style>
效果如图示:

2. 内容无限滚动
<template>
<div class="more">
<div>
<div
v-for="(item, index) in itemNumber"
class="item-li"
:key="index"
></div>
</div>
<div id="more">加载更多...</div>
</div>
</template>
<script>
export default {
name: "More",
data() {
return {
itemNumber: 20,
};
},
mounted() {
// 获取加载更多DOM
const MoreDom = document.getElementById("more");
this.getMore(MoreDom);
},
methods: {
// 加载更多
getMore(MoreDom, preHeight = 200) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
// 从不相交到相交
if (entry.isIntersecting) {
console.log('加载更多');
this.itemNumber += 5;
}
});
},
{
// 根元素面积向下扩大200px
rootMargin: `0px 0px ${preHeight}px 0px`,
}
);
// 开始观察
observer.observe(MoreDom);
},
},
};
</script>
<style scoped>
.item-li {
height: 70px;
vertical-align: middle;
}
.item-li:nth-child(odd) {
background: #2ddf8f;
}
.item-li:nth-child(even) {
background: #83f3bd;
}
</style>
效果如图示:

3. 曝光埋点
<template>
<!-- 横向 -->
<swiper
:slides-per-view="3"
:space-between="50"
@slideChange="onSlideChange"
class="my-swiper"
>
<swiper-slide
:id="index"
class="my-swiper-slide"
v-for="(item, index) in 5"
:key="index"
>Slide {{ index }}</swiper-slide
>
</swiper>
</template>
<script>
import { swiper, swiperSlide } from "vue-awesome-swiper";
import "swiper/dist/css/swiper.css";
export default {
name: "SwiperView",
components: {
swiper,
swiperSlide,
},
mounted() {
const domList = document.querySelectorAll(".my-swiper-slide");
this.change(domList);
},
methods: {
change(domList) {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log("交叉比例大于0");
const { target } = entry;
const isHaveTrack =
target.getAttribute("data-lbgtrack-done");
if (isHaveTrack) {
observer.unobserve(entry.target);
} else {
target.setAttribute("data-lbgtrack-done", "1");
console.log("埋点");
}
}
});
},
{
// 相交程度为50%的时候触发回调函数
threshold: [0.5],
}
);
domList.forEach((item) => {
observer.observe(item);
});
},
onSlideChange() {
console.log("slide change");
},
},
};
</script>
<style scoped>
.my-swiper {
margin-top: 100px;
height: 300px;
background: #83f3bd;
}
.my-swiper1 {
height: 100vh;
}
.my-swiper-slide{
color: #fff;
}
</style>
效果如图:

兼容性
当前浏览器对于IntersectionObserver
的支持性:IntersectionObserver-canuse[8]

IntersectionObserver 的 polyfill
npm install intersection-observer
import 'intersection-observer';

总结
以上就是本次要介绍的全部内容了,总的来说,IntersectionObserver 使用方便快捷有效,所以 赶紧用起来吧~
参考资料
Intersection_Observer_API[9]
作者简介
高艳:猫奴一枚
参考资料
Element.getBoundingClientRect()
: https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect
Element.getBoundingClientRect()
: https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect
viewport: https://developer.mozilla.org/zh-CN/docs/Glossary/Viewport
[4]viewport: https://developer.mozilla.org/zh-CN/docs/Glossary/Viewport
[5]margin
: https://developer.mozilla.org/zh-CN/docs/Web/CSS/margin
root
: https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver/root
margin
: https://developer.mozilla.org/zh-CN/docs/Web/CSS/margin
IntersectionObserver-canuse: https://caniuse.com/?search=IntersectionObserver
[9]Intersection_Observer_API: https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API
作者介绍
