IntersectionObserver
大约 4 分钟
IntersectionObserver
IntersectionObserver 提供了一种异步监听目标元素是否出现在视口的方法,它可以用于判断元素是否可见,从而执行相应的操作。
基本用法
创建 IntersectionObserver 对象
要使用 IntersectionObserver,首先需要创建一个 IntersectionObserver 对象,然后使用该对象来监听目标元素。当目标元素出现在视口或者达到一定阈值时,会触发回调函数,从而执行相应的操作。
const observer = new IntersectionObserver(callback, options);
callback 回调函数
function callback(entries, observer) {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 目标元素可见
} else {
// 目标元素不可见
}
});
}
- entries:一个包含了一组 IntersectionObserverEntry 对象的数组,每个对象代表了一个交叉的目标元素。
- observer:IntersectionObserver 对象。
IntersectionObserverEntry 对象包含了以下属性:
- time:可见性变化的时间,是一个高精度时间戳,单位为毫秒。
- target:可见性变化的目标元素。
- intersectionRatio:可见性变化的比例。
- intersectionRect:目标元素的可见矩形区域。
- boundingClientRect:目标元素的边界矩形区域。
- rootBounds:可见性变化的根元素的边界矩形区域。
- isIntersecting:可见性变化的目标元素是否与视窗交叉。
options 选项
IntersectionObserver 提供了以下常见的选项:
- root:指定根元素,即目标元素所在的容器元素。
- rootMargin:指定根元素的边界距离,可以是一个具有以下格式的字符串:'10px 20px 30px 40px',分别表示上、右、下、左的边界距离。
- threshold:指定监听的目标元素的可见性变化的阈值,可以是一个数字或一个包含多个阈值的数组。
监听目标元素
接下来,需要使用 observe() 方法来监听目标元素。
observer.observe(target);
- target:要监听的目标元素。
当目标元素出现在视口或者达到一定阈值时,就会会触发回调函数
停止监听
当不需要监听目标元素时,可以使用 unobserve() 方法来停止监听。
observer.unobserve(target);
- target:要停止监听的目标元素。
图片懒加载指令
Vue指令实现
export default {
mounted(el: HTMLElement, binding: any) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const lazyImage: HTMLImageElement = entry.target as HTMLImageElement;
lazyImage.src = binding.value;
observer.unobserve(lazyImage);
}
});
});
observer.observe(el);
}
};
在 main.ts 中注册指令
import vLazy from './directives/lazy';
const app = createApp(App);
app.directive('lazy', vLazy);
使用
<template>
<div>
<img v-lazy="'https://picsum.photos/id/237/900/600'" alt="" />
<img v-lazy="'https://picsum.photos/id/238/900/600'" alt="" />
<img v-lazy="'https://picsum.photos/id/239/900/600'" alt="" />
<img v-lazy="'https://picsum.photos/id/240/900/600'" alt="" />
<img v-lazy="'https://picsum.photos/id/241/900/600'" alt="" />
<img v-lazy="'https://picsum.photos/id/242/900/600'" alt="" />
<img v-lazy="'https://picsum.photos/id/243/900/600'" alt="" />
<img v-lazy="'https://picsum.photos/id/244/900/600'" alt="" />
</div>
</template>
<script setup lang="ts"></script>
<style scoped lang="scss">
img {
display: block;
height: 100vh;
}
</style>
无限滚动
<template>
<div>
<ul>
<li v-for="(item, index) in itemList" :key="index">{{ item }}</li>
<li ref="loadingRef">加载中...</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const itemList = ref<Array<string>>([]);
const loadingRef = ref<HTMLLIElement>();
onMounted(() => {
const loadingObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
for (let i = 0; i < 10; i++) {
itemList.value.push('item' + (itemList.value.length + 1));
}
}
});
loadingObserver.observe(loadingRef.value!);
});
</script>
<style scoped lang="scss">
div {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
ul {
width: 80vw;
height: 20vh;
overflow-y: auto;
li {
height: 5vh;
}
}
}
</style>
组件懒加载
<template>
<div ref="rootElement">
<slot v-if="renderComponent" />
<div style="height: 100vh" v-else>加载中...</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
const rootElement = ref(null);
const renderComponent = ref(false);
const emit = defineEmits(['onLoad']);
const props = withDefaults(
defineProps<{
rootMargin?: string;
}>(),
{
rootMargin: '50% 0px'
}
);
let observer: IntersectionObserver;
onMounted(() => {
observer = createIntersectionObserver();
observer.observe(rootElement.value!);
});
onUnmounted(() => {
observer.disconnect();
});
const createIntersectionObserver = () => {
return new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
// console.log(entry);
renderComponent.value = true;
emit('onLoad');
observer.unobserve(entry.target);
}
}
},
{
root: null,
rootMargin: props.rootMargin || '50% 0px'
}
);
};
</script>
<style lang="scss" scoped></style>
使用
<template>
<div>
<LazyLoading @on-load="console.log('load1')">
<img src="https://picsum.photos/id/237/900/600" alt="" />
</LazyLoading>
<LazyLoading @on-load="console.log('load2')">
<img src="https://picsum.photos/id/238/900/600" alt="" />
</LazyLoading>
<LazyLoading @on-load="console.log('load3')">
<img src="https://picsum.photos/id/239/900/600" alt="" />
</LazyLoading>
<LazyLoading @on-load="console.log('load4')">
<img src="https://picsum.photos/id/240/900/600" alt="" />
</LazyLoading>
<LazyLoading @on-load="console.log('load5')">
<img src="https://picsum.photos/id/241/900/600" alt="" />
</LazyLoading>
</div>
</template>
<script setup lang="ts">
import LazyLoading from '@/components/LazyLoading/index.vue';
</script>
<style scoped lang="scss">
img {
display: block;
height: 80vh;
}
</style>
slideIn 效果指令
const distance = 200;
const map = new WeakMap();
function isBelowViewport(el: HTMLElement) {
const rect = el.getBoundingClientRect();
return rect.top > window.innerHeight;
}
const ob = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
// console.log(entry)
const animation = map.get(entry.target);
if (animation && entry.boundingClientRect.top > 0) {
// 出现在视口中且从视口下方进入时,播放动画
animation.play();
ob.unobserve(entry.target);
}
}
}
});
export default {
mounted(el: HTMLElement) {
// 如果元素不在视口下方,不需要动画
if (!isBelowViewport(el)) {
return;
}
/**
* Animations API
* https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Animations_API
* 先向下移动200px,在移动到原位
*/
const animation = el.animate(
[
{
transform: `translateY(${distance}px) scale(1.2)`,
opacity: 0
},
{
marginBottom: 0, // 防止快速下滑最后几个元素异常抖动
transform: 'translateY(0) scale(1)',
opacity: 1
}
],
{
duration: 500,
easing: 'ease-in-out',
fill: 'forwards'
}
);
// 最开始暂停动画
animation.pause();
ob.observe(el);
// 用于出现在视口中时获取动画并播放
map.set(el, animation);
},
unmounted(el: HTMLElement) {
ob.unobserve(el);
}
};
在 main.ts 中注册指令
import vSlideIn from './directives/slideIn';
const app = createApp(App);
app.directive('slide-in', vSlideIn);
使用
<template>
<div class="container">
<div v-slide-in class="item" v-for="(item, index) in 20" :key="index">{{ item }}</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.container {
.item {
height: 40vh;
width: 90vw;
margin: 0 auto;
background-color: skyblue;
text-align: center;
line-height: 40vh;
font-size: 30px;
font-weight: bold;
color: red;
}
.item + .item {
margin-top: 5vh;
}
}
</style>